2009-02-27 12 views
8

Zajmuję się tworzeniem aplikacji w Silverlight2 i próbuję podążać za wzorem Model-View-ViewModel. Wiążę właściwość IsEnabled w niektórych formantach z właściwością boolean w ViewModel.Powiadomienie PropertyChanged dla obliczonych właściwości

Występują problemy, gdy te właściwości pochodzą z innych właściwości. Załóżmy, że mam przycisk Zapisz, który chcę włączyć tylko wtedy, gdy możliwe jest zapisanie (dane zostały załadowane, a my obecnie nie jesteśmy zajęci robieniem danych w bazie danych).

Więc mam kilka właściwości takiego:

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

Teraz to, co chcę zrobić to:

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

jednak uwagę na brak powiadomienia właściciela zmieniło.

Pytanie brzmi: Jaki jest czysty sposób na ujawnienie pojedynczej właściwości boolowskiej, do której mogę się przyłączyć, ale jest obliczany zamiast jawnie ustawionego i dostarcza powiadomienia, aby interfejs mógł być poprawnie aktualizowany?

EDIT:Dzięki za pomoc wszystkim - Mam idzie i miał iść na dokonanie atrybut niestandardowy. Zamieszczam tutaj źródło na wypadek, gdyby ktoś był zainteresowany. Jestem pewien, że można to zrobić w bardziej przejrzysty sposób, więc jeśli zauważysz jakieś błędy, dodaj komentarz lub odpowiedź.

Zasadniczo, co zrobiłem powstał interfejs, który zdefiniowaną listę par klucz-wartość trzymać jakie właściwości zależy od innych właściwości:

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

Potem wykonanych atrybut, aby przejść na każdej nieruchomości:

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

Sam atrybut przechowuje tylko jeden ciąg znaków. Możesz zdefiniować wiele zależności dla każdej właściwości. Tuszem atrybutu jest funkcja statyczna BuildDependentPropertyList. Musisz zadzwonić do tego w konstruktorze swojej klasy. (Czy ktoś wie, czy można to zrobić za pomocą atrybutu class/constructor?) W moim przypadku wszystko to jest ukryte w klasie bazowej, więc w podklasach wystarczy umieścić atrybuty we właściwościach. Następnie modyfikujesz odpowiednik OnPropertyChanged, aby wyszukać zależności. Oto moja podstawowa klasa ViewModel jako przykład:

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

Na koniec ustawiasz atrybuty na właściwościach, których dotyczy problem. Podoba mi się ten sposób, ponieważ właściwość pochodna przechowuje właściwości, od których zależy, a nie na odwrót.

[NotifyDependsOn("Session")] 
    [NotifyDependsOn("DatabaseBusy")] 
    public bool SaveEnabled 
    { 
     get { return !this.Session.IsLocked && !this.DatabaseBusy; } 
    } 

Wielkim zastrzeżeniem jest to, że działa tylko wtedy, gdy inne nieruchomości są członkami obecnej klasy. W powyższym przykładzie, jeśli zmiana this.Session.IsLocked, powiadomienie nie zostanie wykonane. Sposób, w jaki to obejmuję, polega na zasubskrybowaniu tego Session.NotifyPropertyChanged i fire PropertyChanged dla "Session".(Tak, to doprowadziłoby do wydarzeń wypalania gdzie zrobił muszą)

Odpowiedz

6

Tradycyjnym sposobem na to jest dodanie OnPropertyChanged połączenia do każdej z właściwości, które mogą mieć wpływ na obliczane jeden, tak:

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

to można się nieco brudny (jeśli, na przykład, twoje obliczenia w zmianach CanSave).

One (odkurzacz nie wiem?) Sposób aby obejść ten problem byłoby zastąpić OnPropertyChanged i nawiązać połączenie tam:

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

Zamierzam przejść do nadpisania OnPropertyChanged. Zastanawiam się, czy zależności można zadeklarować jako atrybuty? – geofftnz

+0

Możesz ustawić statyczną listę , aby śledzić nazwy właściwości, od których zależy CanSave. Wtedy byłby to pojedynczy "if (_canSaveProperties.Contains (propertyName))". –

+0

Dobry pomysł, dzięki! – geofftnz

2

Trzeba dodać powiadomienie o zmianie własności CanSave wszędzie jedną z właściwości zależy zmiany:

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

I

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

to będzie działać poprawnie, ale to oznacza, że ​​będzie unieważniania CanSave więcej niż jest to konieczne, może sprawić, że reszta aplikacji wykona więcej pracy niż jest to wymagane. – KeithMahoney

+0

Co zrobić, jeśli nie mam dostępu do ustalonej metody właściwości? – geofftnz

1

Co z tym rozwiązaniem?

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

Następnie wywołaj funkcję UpdateCanSave() w ustawieniach IsLoaded i DatabaseBusy?

Jeśli nie możesz zmodyfikować setterów IsLoaded i DatabaseBusy, ponieważ znajdują się one w różnych klasach, możesz spróbować wywołać funkcję UpdateCanSave() w module obsługi zdarzenia PropertyChanged dla obiektu definiującego IsLoaded i DatabaseBusy.

Powiązane problemy