Уведомление 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)

Решение

Традиционный способ сделать это - добавить вызов 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");
    }
}

Вам необходимо добавить уведомление об изменении свойства CanSave везде, где изменяется одно из свойств, от которых оно зависит:

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

А также

OnPropertyChanged("IsEnabled");
OnPropertyChanged("CanSave");

Как насчет этого решения?

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

Затем вызовите UpdateCanSave () в установщиках IsLoaded и DatabaseBusy?

Если вы не можете изменить установщики IsLoaded и DatabaseBusy, потому что они находятся в разных классах, вы можете попробовать вызвать UpdateCanSave () в обработчике событий PropertyChanged для объекта, определяющего IsLoaded и DatabaseBusy.