2009-02-27 9 views
8

Ich entwickle eine Anwendung in Silverlight2 und versuche, das Model-View-ViewModel-Muster zu folgen. Ich binde die IsEnabled-Eigenschaft für einige Steuerelemente auf eine boolesche Eigenschaft im ViewModel.PropertyChanged Benachrichtigung für berechnete Eigenschaften

Ich habe Probleme, wenn diese Eigenschaften von anderen Eigenschaften abgeleitet sind. Angenommen, ich habe eine Schaltfläche "Speichern", die nur aktiviert werden soll, wenn es möglich ist, Daten zu speichern (Daten wurden geladen, und wir sind gerade nicht damit beschäftigt, Daten in der Datenbank zu speichern).

So habe ich ein paar Eigenschaften wie folgt aus:

private bool m_DatabaseBusy; 
    public bool DatabaseBusy 
    { 
     get { return m_DatabaseBusy; } 
     set 
     { 
      if (m_DatabaseBusy != value) 
      { 
       m_DatabaseBusy = value; 
       OnPropertyChanged("DatabaseBusy"); 
      } 
     } 
    } 

    private bool m_IsLoaded; 
    public bool IsLoaded 
    { 
     get { return m_IsLoaded; } 
     set 
     { 
      if (m_IsLoaded != value) 
      { 
       m_IsLoaded = value; 
       OnPropertyChanged("IsLoaded"); 
      } 
     } 
    } 

Jetzt möchte ich dies tun, was ist:

public bool CanSave 
{ 
    get { return this.IsLoaded && !this.DatabaseBusy; } 
} 

Aber beachten Sie den Mangel an eigenschafts geändert Benachrichtigung. Die Frage ist also: Was ist eine saubere Möglichkeit, eine einzelne boolesche Eigenschaft zu offenbaren, an die ich binden kann, die aber berechnet wird, anstatt explizit festgelegt zu werden, und Benachrichtigungen bereitstellt, damit die Benutzeroberfläche korrekt aktualisiert werden kann?

EDIT:Danke für die Hilfe jeder - Ich habe es gehen und hatte ein Ziel bei der Erstellung eines benutzerdefinierten Attributs. Ich poste hier die Quelle, falls jemand interessiert ist. Ich bin mir sicher, dass es auf eine sauberere Art und Weise getan werden könnte. Wenn Sie Fehler sehen, fügen Sie einen Kommentar oder eine Antwort hinzu.

Im Grunde, was ich tat, war eine Schnittstelle gemacht, die eine Liste von Schlüssel-Wert-Paare definiert zu halten, welche Eigenschaften auf andere Eigenschaften abhängig:

public interface INotifyDependentPropertyChanged 
{ 
    // key,value = parent_property_name, child_property_name, where child depends on parent. 
    List<KeyValuePair<string, string>> DependentPropertyList{get;} 
} 

ich dann das Attribut auf jede Eigenschaft gehen gemacht:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = false)] 
public class NotifyDependsOnAttribute : Attribute 
{ 
    public string DependsOn { get; set; } 
    public NotifyDependsOnAttribute(string dependsOn) 
    { 
     this.DependsOn = dependsOn; 
    } 

    public static void BuildDependentPropertyList(object obj) 
    { 
     if (obj == null) 
     { 
      throw new ArgumentNullException("obj"); 
     } 

     var obj_interface = (obj as INotifyDependentPropertyChanged); 

     if (obj_interface == null) 
     { 
      throw new Exception(string.Format("Type {0} does not implement INotifyDependentPropertyChanged.",obj.GetType().Name)); 
     } 

     obj_interface.DependentPropertyList.Clear(); 

     // Build the list of dependent properties. 
     foreach (var property in obj.GetType().GetProperties()) 
     { 
      // Find all of our attributes (may be multiple). 
      var attributeArray = (NotifyDependsOnAttribute[])property.GetCustomAttributes(typeof(NotifyDependsOnAttribute), false); 

      foreach (var attribute in attributeArray) 
      { 
       obj_interface.DependentPropertyList.Add(new KeyValuePair<string, string>(attribute.DependsOn, property.Name)); 
      } 
     } 
    } 
} 

Das Attribut selbst speichert nur eine einzige Zeichenfolge. Sie können mehrere Abhängigkeiten pro Eigenschaft definieren. Die Eingeweide des Attributs befinden sich in der statischen Funktion BuildDependentPropertyList. Sie müssen dies im Konstruktor Ihrer Klasse aufrufen. (Wer weiß, ob es eine Möglichkeit gibt, dies über ein class/constructor-Attribut zu tun?) In meinem Fall ist das alles in einer Basisklasse versteckt, also fügen Sie in den Unterklassen die Attribute einfach in die Eigenschaften ein. Dann ändern Sie Ihr OnPropertyChanged-Äquivalent, um nach Abhängigkeiten zu suchen. Hier ist meine ViewModel-Basisklasse als Beispiel:

public class ViewModel : INotifyPropertyChanged, INotifyDependentPropertyChanged 
{ 
    public event PropertyChangedEventHandler PropertyChanged; 
    protected virtual void OnPropertyChanged(string propertyname) 
    { 
     if (PropertyChanged != null) 
     { 
      PropertyChanged(this, new PropertyChangedEventArgs(propertyname)); 

      // fire for dependent properties 
      foreach (var p in this.DependentPropertyList.Where((x) => x.Key.Equals(propertyname))) 
      { 
       PropertyChanged(this, new PropertyChangedEventArgs(p.Value)); 
      } 
     } 
    } 

    private List<KeyValuePair<string, string>> m_DependentPropertyList = new List<KeyValuePair<string, string>>(); 
    public List<KeyValuePair<string, string>> DependentPropertyList 
    { 
     get { return m_DependentPropertyList; } 
    } 

    public ViewModel() 
    { 
     NotifyDependsOnAttribute.BuildDependentPropertyList(this); 
    } 
} 

Schließlich legen Sie die Attribute für die betroffenen Eigenschaften. Ich mag diesen Weg, weil die abgeleitete Eigenschaft die Eigenschaften hält, von denen sie abhängt, und nicht umgekehrt.

Der große Nachteil hier ist, dass es nur funktioniert, wenn die anderen Eigenschaften Mitglieder der aktuellen Klasse sind. Wenn sich im obigen Beispiel this.Session.IsLocked ändert, wird die Benachrichtigung nicht ausgeführt. Die Art und Weise, wie ich das umgehe, besteht darin, diese.Session.NotifyPropertyChanged zu abonnieren und PropertyChanged für "Session" zu setzen.(Ja, würde dies in Ereignissen führen befeuert werden, sofern sie didnt brauchen)

Antwort

6

Der traditionelle Weg, dies zu tun, ist ein OnPropertyChanged Aufruf an jeden der Eigenschaften hinzuzufügen, die Ihre berechnet man, wie dies beeinflussen könnten:

public bool IsLoaded 
{ 
    get { return m_IsLoaded; } 
    set 
    { 
     if (m_IsLoaded != value) 
     { 
      m_IsLoaded = value; 
      OnPropertyChanged("IsLoaded"); 
      OnPropertyChanged("CanSave"); 
     } 
    } 
} 

Dies ist ein bisschen chaotisch bekommen kann (wenn zum Beispiel, Ihre Berechnung in CanSave ändert sich).

One (Reiniger Ich weiß nicht?) Weg, dies zu umgehen wäre OnPropertyChanged außer Kraft zu setzen und den Anruf dort zu machen:

protected override void OnPropertyChanged(string propertyName) 
{ 
    base.OnPropertyChanged(propertyName); 
    if (propertyName == "IsLoaded" /* || propertyName == etc */) 
    { 
     base.OnPropertyChanged("CanSave"); 
    } 
} 
+0

Ich werde für die OnPropertyChanged Überschreibung gehen. Ich frage mich, ob die Abhängigkeiten als Attribute deklariert werden könnten? – geofftnz

+0

Sie könnten eine statische Liste einrichten, um die Namen der Eigenschaften zu überwachen, von denen CanSave abhängig ist. Dann wäre es ein einziges "if (_canSaveProperties.Contains (propertyName))". –

+0

Gute Idee, danke! – geofftnz

2

Sie benötigen eine Benachrichtigung für die CanSave Eigenschaftsänderung eines der überall Eigenschaften hinzufügen es hängt Änderungen:

OnPropertyChanged("DatabaseBusy"); 
OnPropertyChanged("CanSave"); 

Und

OnPropertyChanged("IsEnabled"); 
OnPropertyChanged("CanSave"); 
+0

Dies richtig funktionieren würde, aber es bedeutet, dass Sie CanSave ungültig gemacht werden mehr als notwendig ist, wodurch der Rest der Anwendung möglicherweise mehr Arbeit leistet als erforderlich ist. – KeithMahoney

+0

Was passiert, wenn ich keinen Zugriff auf die Set-Methode der Eigenschaft habe? – geofftnz

1

Wie wäre es mit dieser Lösung?

private bool _previousCanSave; 
private void UpdateCanSave() 
{ 
    if (CanSave != _previousCanSave) 
    { 
     _previousCanSave = CanSave; 
     OnPropertyChanged("CanSave"); 
    } 
} 

Dann UpdateCanSave() in den Setter von IsLoaded und DatabaseBusy?

Wenn Sie die Setter von IsLoaded und DatabaseBusy nicht ändern können, weil sie sich in verschiedenen Klassen befinden, können Sie versuchen, UpdateCanSave() im PropertyChanged-Ereignishandler für das Objekt zu verwenden, das IsLoaded und DatabaseBusy definiert.