PropertyChanged уведомление для рассчитанных свойств - PullRequest
8 голосов
/ 27 февраля 2009

Я разрабатываю приложение в 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. Вы должны вызвать это в конструкторе вашего класса. (Кто-нибудь знает, есть ли способ сделать это через атрибут class / constructor?) В моем случае все это скрыто в базовом классе, поэтому в подклассах вы просто помещаете атрибуты в свойства. Затем вы модифицируете свой эквивалент 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 ]

6 голосов
/ 27 февраля 2009

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

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

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

И

OnPropertyChanged("IsEnabled");
OnPropertyChanged("CanSave");
1 голос
/ 27 февраля 2009

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

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

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

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

...