2016-09-14 2 views
0

Ceci est ma première question, je m'excuse si elle n'est pas parfaitement formatée. Je suis nouveau à WPF et MVVM et j'ai rencontré un problème que je n'arrive pas à comprendre.Prévention des boucles infinies lors de l'utilisation de INotifyPropertyChanged avec une arborescence WPF contenant des cases à cocher

J'ai une vue arborescente qui affiche une hiérarchie MenuItem avec une case à cocher par MenuItem, à la fois pour les nœuds parent et enfant. La solution actuelle permet à un utilisateur de cliquer sur un nœud parent et tous les éléments enfants sont cochés/décochés si nécessaire.

Je dois maintenant mettre en œuvre l'inverse de cela, où si un utilisateur clique sur l'un des nœuds enfants, le nœud parent doit être sélectionné s'il n'est pas déjà sélectionné. Le problème que j'ai actuellement est que la vérification du nœud parent déclenche par programmation l'événement INotifiedPropertyChanged pour le nœud parent qui revérifie mes nœuds enfants.

Comment puis-je empêcher cela?

Voici mon code MenuItem:

public class MenuItem : INotifyPropertyChanged 
    { 
     string _name; 
     List<MenuItem> _subItems = new List<MenuItem>(); 
     bool _isChecked; 
     MenuItem _parent; 

     public List<MenuItem> SubItems 
     { 
      get { return _subItems; } 
      set 
      { 
       _subItems = value; 
       RaisePropertyChanged("SubItems"); 
      } 
     } 

     public string Name 
     { 
      get { return _name; } 
      set 
      { 
       _name = value; 
       RaisePropertyChanged("Name"); 
      } 
     } 

     public bool IsChecked 
     { 
      get { return _isChecked; } 
      set 
      { 
       _isChecked = value; 
       RaisePropertyChanged("IsChecked"); 
      } 
     } 

     public MenuItem Parent 
     { 
      get { return _parent; } 
      set 
      { 
       _parent = value; 
       RaisePropertyChanged("Parent"); 
      } 
     } 

     public event PropertyChangedEventHandler PropertyChanged; 
     private void RaisePropertyChanged(string propertyName) 
     { 
      if (PropertyChanged != null) 
       PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); 

      if (propertyName == "IsChecked") 
      { 
       if (Parent == null) 
       { 
        foreach (MenuItem Child in _subItems) 
         Child.IsChecked = this.IsChecked; 
       } 

       //if (Parent != null) 
       //{ 
       // Parent.IsChecked = IsChecked ? true :Parent.IsChecked; 
       //} 
      } 
     } 
    } 

Le code commenté ci-dessus est l'endroit où je rencontre l'erreur.

Toute indication sera grandement appréciée.

+0

vous pourriez essayer de ne déclencher l'événement que si la valeur est réellement différente de la valeur actuelle – Nico

+0

La valeur doit toujours être différente. J'ai fondamentalement besoin que l'événement PropertyChanged se déclenche uniquement s'il n'est pas appelé par une méthode. – ThatChris

+2

ce n'est pas la réponse, mais un commentaire préliminaire sur votre approche. N'implémentez pas cette logique dans RaisePropertyChanged! À mon humble avis, il est plus approprié de mettre en œuvre cette demande dans les vérificateurs correspondant ... alors, bien sûr, la réponse concrète devrait suivre ... –

Répondre

1

Juste un peu réponse plus élaborée en fonction de celui qui est déjà écrit par OP

public bool IsChecked 
    { 
     get { return _isChecked; } 
     set 
     { 
      _isChecked = value; 

      if (_parent == null) 
      { 
       foreach (MenuItem Child in _subItems) 
       { 
        Child._isChecked = this._isChecked; 
        Child.RaisePropertyChanged("IsChecked"); 
       } 
      } 

      if (_parent != null) 
      { 
       _parent.NotifyChecked(_isChecked); 
      } 

      RaisePropertyChanged("IsChecked"); 
     } 
    } 
    public void NotifyChecked(bool childChecked) 
    { 
     _isChecked = childChecked; 
     RaisePropertyChanged("IsChecked"); 
     if (_parent != null) 
     { 
      _parent.NotifyChecked(_isChecked); 
     } 
    } 
0

Je pense que vous avez besoin d'une autre propriété à stocker si l'un des enfants est coché. Quelque chose comme IsChildChecked.

Dans l'interface utilisateur, vous pouvez lier ces deux propriétés (IsChecked et IsChildChecked) à IsChecked du noeud avec MultiBinding. Utilisez un convertisseur pour le définir.

+0

Le long des lignes droites, mais ayant une propriété locale contenant l'état externe des enfants n'est pas une bonne idée, mieux vaut utiliser un calcul dynamique comme ' isChecked || Children.Any (c => IsChecked); ' – MikeT

0

commentaire de Machine Learning m'a conduit à la réponse:

public bool IsChecked 
     { 
      get { return _isChecked; } 
      set 
      { 
       _isChecked = value; 

       if (_parent == null) 
       { 
        foreach (MenuItem Child in _subItems) 
        { 
         Child._isChecked = this._isChecked; 
         Child.RaisePropertyChanged("IsChecked"); 
        } 
       } 

       if (_parent != null) 
       { 
        _parent._isChecked = _isChecked ? true : _parent._isChecked; 
        _parent.RaisePropertyChanged("IsChecked"); 
       } 

       RaisePropertyChanged("IsChecked"); 
      } 
     } 

Déplacement du code au setter au lieu de la manipulation en cas travaillé pour moi.

+0

Alors vous avez rendu public le champ sous-jacent _isChecked? Ce n'est pas bien .. – lvoros

+0

Actuellement, mon modèle MenuItem et ViewModel sont toujours contenus dans une classe, ce qui explique pourquoi le _isChecked n'a pas besoin d'être privé. La prochaine étape serait de séparer cela. – ThatChris

+1

Je pense que ce code ne fonctionne pas comme envoyé ... si vous définissez le champ _isChecked du parent, NotifyPropertyChanged ne se déclenchera pas, donc l'interface utilisateur ne s'affichera pas si le parentnode est coché. Ou est-ce que je le vois faux? – lvoros

0

il y a quelques approches différentes que vous pouvez prendre,

1 Calculer est vérifié la propriété du parent

cela fonctionnerait par le parent écoutant l'événement PropertyChanged de l'enfant et si l'un d'entre eux est vrai retournant true pour les parents IsChecked

private bool isChecked; 
public bool IsChecked 
{ 
    get{ return isChecked || Children.Any(c=>IsChecked);} 
    set 
    { 
     isChecked = value; 
     RaisePropertyChanged("IsChecked"); 
     foreach(var child in Children)child.IsChecked 
    } 
} 
public void Child_PropertyChanged(object sender,PropertyChangedEventArgs e) 
{ 
    if(e.PropertyName == "IsChecked") 
     RaisePropertyChanged("IsChecked"); 
} 

cette approche a l'avantage de maintenir les parents, cliquez sur l'état indépendamment

2 flip 1 tour et calculer la propriété de l'enfant IsChecked

private bool isChecked; 
public bool IsChecked 
{ 
    get{ return isChecked || Parent.IsChecked;} 
    set 
    { 
     isChecked = value; 
     RaisePropertyChanged("IsChecked"); 
    } 
} 
public void Parent_PropertyChanged(object sender,PropertyChangedEventArgs e) 
{ 
    if(e.PropertyName == "IsChecked") 
     RaisePropertyChanged("IsChecked"); 
} 

3 Créez une seconde voie pour changer l'état avec en déclenchant la cascade

private bool isChecked; 
public bool IsChecked 
{ 
    get{ return isChecked;} 
    set 
    { 
     SetIsChecked(value); 
     foreach(var child in Children)Parent.SetIsChecked(isChecked) 
    } 
} 
public void SetIsChecked(bool value) 
{ 
    isChecked = value; 
    RaisePropertyChanged("IsChecked"); 
} 

cette façon aussi longtemps que les enfants appellent la méthode SetIsChecked directement alors la cascade est seulement tr iggered sur un lorsque le parent est réglé directement par le poseur

Note: dans votre code que vous n'êtes pas traiter l'événement PropertyChanged vous élevez seulement

manipulation

ressemble à ce

public MenuItem Parent 
{ 
    get { return _parent; } 
    set 
    { 
     //remove old handler 
     // this stops listening to the old parent if there is one 
     if(_parent != null) 
      _parent.PropertyChange-=Parent_PropertyChanged; 

     //notice that the value of _parent changes here so _parent above is not the same as _parent used below 
     _parent = value; 

     //add new handler 
     // this starts listening to the new parent if there is one 
     if(_parent != null) 
      _parent.PropertyChange+=Parent_PropertyChanged; 

     RaisePropertyChanged("Parent"); 
    } 
} 
//handler 
public void Parent_PropertyChanged(object sender,PropertyChangedEventArgs e) 
{ 
    if(e.PropertyName == "IsChecked") 
     RaisePropertyChanged("IsChecked"); 
} 

également tout ce qui précède peut être amélioré en vérifiant si la valeur actuelle a changé avant d'apporter des modifications

+0

Salut Mike, merci pour la réponse. Juste une question rapide. . .la manipulation vs la montée de l'événement. Quelle est la différence entre le 2 et ce qui est gagné en supprimant le gestionnaire et en l'ajoutant à nouveau après que la propriété a été changée? – ThatChris

+0

c'est la différence entre parler et écouter, quand vous déclenchez un événement vous criez "THIS HAPPENED" alors s'il y a un hander écoutant pour ce shout il fera quelque chose quand il l'entend, alors 'RaisePropertyChanged (" IsChecked ")' est le Shout une propriété a changé et '_parent.PropertyChange + = Parent_PropertyChanged;' dit à votre classe que lorsque vous entendez le parent crier sur le changement de propriété que vous voulez à ce qui est dans 'Parent_PropertyChanged' – MikeT

+0

Le gestionnaire est lié à l'objet, si vous changer le parent sans enlever le gestionnaire du parent précédent, alors vous écouterez à la fois le nouveau parent et le vieux parent – MikeT