使用MVVM更新依赖属性

9

我的viewmodel上有一些属性:

public ObservableCollection<Task> Tasks { get; set; }

public int Count
{
    get { return Tasks.Count; }
}

public int Completed
{
    get { return Tasks.Count(t => t.IsComplete); }
}

Tasks 发生变化时,最好的更新这些属性的方法是什么?

我目前的方法:

public TaskViewModel()
{
    Tasks = new ObservableCollection<Task>(repository.LoadTasks());
    Tasks.CollectionChanged += (s, e) => 
        {
            OnPropertyChanged("Count");
            OnPropertyChanged("Completed");
        };
}

有没有更优雅的方法来做这件事?
2个回答

9

关于Count,您根本不需要这样做。只需绑定到Tasks.Count,您的绑定将通过ObservableCollection被通知更改。

Completed则是另一回事,因为它位于ObservableCollection之外。但从抽象/接口层面来看,您确实希望Completed成为Tasks集合的属性。

为此,我认为更好的方法是为您的Tasks属性创建“子”视图模型:

public class TasksViewModel : ObservableCollection<Task>
{
    public int Completed
    {
        get { return this.Count(t => t.IsComplete); }
    }

    protected override void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        base.OnPropertyChanged(e);
        if(e.PropertyName == "Count") NotifyCompletedChanged();
    }

    protected override void OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        base.OnCollectionChanged(e);
        NotifyCompletedChanged();
    }

    void NotifyCompletedChanged()
    {
        OnPropertyChanged(_completedChangedArgs);
    }
    readonly PropertyChangedEventArgs _completedChangedArgs = new PropertyChangedEventArgs("Completed");
}

这样做可以让您获得ObservableCollection的所有好处,并有效地使Completed属性成为其一部分。我们仍然没有仅捕获完成项目数量真正更改的情况,但我们已经在某种程度上减少了冗余通知的数量。
现在,视图模型只有这个属性:
public TasksViewModel Tasks { get; set; }

…你可以轻松绑定到TasksTasks.CountTasks.Completed


作为替代方案,如果你更愿意在“主”视图模型上创建这些其他属性,你可以采用子类化的ObservableCollection<T>概念来创建一个带有某些方法的集合,其中你可以传递一个Action<string>委托,它将表示在主视图模型上引发属性更改通知,以及一些属性名称列表。然后,该集合就可以有效地在视图模型上引发属性更改通知:

public class ObservableCollectionWithSubscribers<T> : ObservableCollection<T>
{
    Action<string> _notificationAction = s => { }; // do nothing, by default
    readonly IList<string> _subscribedProperties = new List<string>();

    public void SubscribeToChanges(Action<string> notificationAction, params string[] properties)
    {
        _notificationAction = notificationAction;

        foreach (var property in properties)
            _subscribedProperties.Add(property);
    }


    protected override void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        base.OnPropertyChanged(e);
        NotifySubscribers();
    }

    protected override void OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        base.OnCollectionChanged(e);
        NotifySubscribers();
    }

    void NotifySubscribers()
    {
        foreach (var property in _subscribedProperties)
            _notificationAction(property);
    }
}

您甚至可以将属性类型保留为ObservableCollection<Task>
public class ViewModel : INotifyPropertyChanged
{
    public ViewModel()
    {
        var tasks = new ObservableCollectionWithSubscribers<Task>();
        tasks.SubscribeToChanges(Notify, "Completed");
        Tasks = tasks;
    }

    public ObservableCollection<Task> Tasks { get; private set; }

    public int Completed
    {
        get { return Tasks.Count(t => t.IsComplete); }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    void Notify(string property)
    {
        var handler = PropertyChanged;
        if(handler != null) handler(this, new PropertyChangedEventArgs(property));
    }
}

1
那肯定是一个更好的设计。 - Joel B Fant

4

对我来说看起来相当优雅。我真的不知道你如何使其更加简洁。

(写出这样的答案有点奇怪。如果有人真的想到了更优雅的方法,我可能会删除这个回答。)

好的,我注意到一件事,与原问题无关:您的Tasks属性具有公共的setter。将其设置为private set;,或者您将需要使用后备字段实现set,以便您可以删除先前实例上的委托,替换并连接新实例,并使用“Tasks”、“Count”和“Completed”执行OnPropertyChanged(并且看到Tasks在构造函数中设置,我猜private set;是更好的选择。)

这并没有使得通知CountCompleted更加优雅,但它修复了一个错误。

许多MVVM框架从lambda表达式中获取属性名称,因此,您可以编写OnPropertyChanged(() => Count)而不是OnPropertyChanged("Count"),以便它将遵循重构工具帮助进行的重命名。虽然我认为重命名并不经常发生,但它确实避免了一些字符串字面量。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接