Уведомление PropertyChanged для вычисляемых свойств
Я разрабатываю приложение в Silverlight2 и пытаюсь следовать шаблону Model-View-ViewModel. Я привязываю свойство IsEnabled некоторых элементов управления к логическому свойству ViewModel.
Я сталкиваюсь с проблемами, когда эти свойства являются производными от других свойств. Допустим, у меня есть кнопка «Сохранить», которую я хочу активировать только тогда, когда ее можно сохранить (данные загружены, и в настоящее время мы не заняты работой с базой данных).
Итак, у меня есть несколько таких свойств:
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");
}
}
}
Теперь я хочу сделать следующее:
public bool CanSave
{
get { return this.IsLoaded && !this.DatabaseBusy; }
}
Но обратите внимание на отсутствие уведомления об изменении свойства.
Итак, возникает вопрос: каков чистый способ раскрытия одного логического свойства, к которому я могу привязаться, но рассчитывается вместо того, чтобы быть явно установленным, и предоставляет уведомление, чтобы пользовательский интерфейс мог правильно обновляться?
РЕДАКТИРОВАТЬ: Спасибо за помощь всем - у меня все получилось, и я попытался создать настраиваемый атрибут. Я размещаю здесь источник, если кому-то интересно. Я уверен, что это можно было бы сделать более чистым способом, поэтому, если вы заметите какие-либо недостатки, добавьте комментарий или ответ.
По сути, то, что я сделал, было созданием интерфейса, который определял список пар ключ-значение для хранения того, какие свойства зависят от других свойств:
public interface INotifyDependentPropertyChanged
{
// key,value = parent_property_name, child_property_name, where child depends on parent.
List<KeyValuePair<string, string>> DependentPropertyList{get;}
}
Затем я сделал атрибут для каждого свойства:
[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));
}
}
}
}
Сам атрибут хранит только одну строку. Вы можете определить несколько зависимостей для каждого свойства. Основа атрибута находится в статической функции BuildDependentPropertyList. Вы должны вызвать это в конструкторе вашего класса. (Кто-нибудь знает, есть ли способ сделать это с помощью атрибута класса / конструктора?) В моем случае все это скрыто в базовом классе, поэтому в подклассах вы просто помещаете атрибуты в свойства. Затем вы изменяете свой эквивалент OnPropertyChanged для поиска любых зависимостей. Вот мой базовый класс ViewModel в качестве примера:
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);
}
}
Наконец, вы устанавливаете атрибуты для затронутых свойств. Мне нравится этот способ, потому что производное свойство содержит свойства, от которых оно зависит, а не наоборот.
[NotifyDependsOn("Session")]
[NotifyDependsOn("DatabaseBusy")]
public bool SaveEnabled
{
get { return !this.Session.IsLocked && !this.DatabaseBusy; }
}
Большое предостережение здесь в том, что он работает только тогда, когда другие свойства являются членами текущего класса. В приведенном выше примере, если this.Session.IsLocked изменяется, уведомление не проходит. Я могу обойти это, подписавшись на this.Session.NotifyPropertyChanged и активируя PropertyChanged для "Session". (Да, это приведет к срабатыванию событий там, где в этом нет необходимости)
Ответов (3)3
Традиционный способ сделать это - добавить вызов OnPropertyChanged к каждому из свойств, которые могут повлиять на ваше вычисленное, например:
public bool IsLoaded
{
get { return m_IsLoaded; }
set
{
if (m_IsLoaded != value)
{
m_IsLoaded = value;
OnPropertyChanged("IsLoaded");
OnPropertyChanged("CanSave");
}
}
}
Это может немного запутаться (если, например, ваш расчет в CanSave изменится).
Один (более чистый? Я не знаю) способ обойти это - переопределить OnPropertyChanged и выполнить вызов там:
protected override void OnPropertyChanged(string propertyName)
{
base.OnPropertyChanged(propertyName);
if (propertyName == "IsLoaded" /* || propertyName == etc */)
{
base.OnPropertyChanged("CanSave");
}
}
Как насчет этого решения?
private bool _previousCanSave;
private void UpdateCanSave()
{
if (CanSave != _previousCanSave)
{
_previousCanSave = CanSave;
OnPropertyChanged("CanSave");
}
}
Затем вызовите UpdateCanSave () в установщиках IsLoaded и DatabaseBusy?
Если вы не можете изменить установщики IsLoaded и DatabaseBusy, потому что они находятся в разных классах, вы можете попробовать вызвать UpdateCanSave () в обработчике событий PropertyChanged для объекта, определяющего IsLoaded и DatabaseBusy.