2009-02-27 8 views
8

Estoy desarrollando una aplicación en Silverlight2 e intento seguir el patrón Model-View-ViewModel. Estoy vinculando la propiedad IsEnabled en algunos controles a una propiedad booleana en ViewModel.Notificación PropertyChanged para las propiedades calculadas

Tengo problemas cuando esas propiedades se derivan de otras propiedades. Digamos que tengo un botón Guardar que solo quiero habilitar cuando es posible guardar (se han cargado datos, y actualmente no estamos ocupados haciendo cosas en la base de datos).

así que tengo un par de propiedades de esta manera:

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

Ahora lo que quiero hacer es esto:

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

Pero tenga en cuenta la falta de notificación de la propiedad cambiado.

Entonces la pregunta es: ¿Cuál es una manera limpia de exponer una sola propiedad booleana a la que puedo vincular, pero se calcula en lugar de establecerla explícitamente y proporciona notificaciones para que la UI pueda actualizarse correctamente?

EDIT:Gracias por la ayuda de todos, lo hice funcionar e hice un intento para crear un atributo personalizado. Estoy publicando la fuente aquí en caso de que alguien esté interesado. Estoy seguro de que se podría hacer de una manera más limpia, por lo que si ve algún defecto, agregue un comentario o una respuesta.

Básicamente lo que hice fue hecha una interfaz que define una lista de pares de valores clave para mantener las propiedades que dependen de otras propiedades:

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

Entonces hice el atributo de ir en cada propiedad:

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

El atributo en sí solo almacena una sola cadena. Puede definir múltiples dependencias por propiedad. Las agallas del atributo están en la función estática BuildDependentPropertyList. Tienes que llamar esto al constructor de tu clase. (¿Alguien sabe si hay una manera de hacerlo a través de un atributo de clase/constructor?) En mi caso, todo esto está oculto en una clase base, por lo que en las subclases simplemente colocas los atributos en las propiedades. A continuación, modifique su equivalente OnPropertyChanged para buscar cualquier dependencia. Aquí está mi clase base de ViewModel como un ejemplo:

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

Finalmente, establece los atributos en las propiedades afectadas. Me gusta de esta manera porque la propiedad derivada tiene las propiedades de las que depende, y no al revés.

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

La gran advertencia aquí es que solo funciona cuando las otras propiedades son miembros de la clase actual. En el ejemplo anterior, si this.Session.IsLocked cambia, la notificación no se procesa. La forma en que soluciono esto es suscribiéndome a esto. Session.NotifyPropertyChanged y disparo PropertyChanged para "Session".(Sí, esto resultaría en eventos disparando donde hizo falta a)

Respuesta

6

La forma tradicional de hacer esto es añadir una llamada OnPropertyChanged a cada una de las propiedades que podrían afectar a su calculada uno, como esto:

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

Esto puede ser un poco desordenado (si, por ejemplo, su cálculo en CanSave cambia).

Uno (limpiador no sé?) Manera de evitar esto sería para anular OnPropertyChanged y hacer la llamada allí:

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

Iré por la anulación de OnPropertyChanged. Me pregunto si las dependencias podrían declararse como atributos. – geofftnz

+0

Puede configurar una lista estática para realizar un seguimiento de los nombres de propiedad de los que depende CanSave. Entonces sería un solo "if (_canSaveProperties.Contains (propertyName))". –

+0

¡Buena idea, gracias! – geofftnz

2

Es necesario añadir una notificación para el cambio de propiedad CanSave todas partes una de las propiedades que depende cambios:

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

Y

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

Esto funcionaría correctamente, pero significa que usted que invalide los CanSave más de lo necesario, posiblemente haciendo que el resto de la aplicación haga más trabajo de lo requerido. – KeithMahoney

+0

¿Qué sucede si no tengo acceso al método establecido de la propiedad? – geofftnz

1

¿Qué tal esta solución?

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

¿Luego llama a UpdateCanSave() en los reguladores de IsLoaded y DatabaseBusy?

Si no puede modificar los setters de IsLoaded y DatabaseBusy porque están en clases diferentes, puede intentar llamar a UpdateCanSave() en el controlador de eventos PropertyChanged para el objeto que define IsLoaded y DatabaseBusy.

Cuestiones relacionadas