Some properties on my viewmodel
:
public ObservableCollection<Task> Tasks { get; set; }
public int Count
{
get { return Tasks.Count; }
}
public int Completed
{
get { return Tasks.Count(t => t.IsComplete); }
}
What's the best way to update these properties when Tasks
changes?
My current method:
public TaskViewModel()
{
Tasks = new ObservableCollection<Task>(repository.LoadTasks());
Tasks.CollectionChanged += (s, e) =>
{
OnPropertyChanged("Count");
OnPropertyChanged("Completed");
};
}
Is there a more elegant way to do this?
With respect to Count
, you don't have to do this at all. Simply bind to Tasks.Count
and your bindings will get notified of the change by the ObservableCollection
.
Completed
is a different story, because this is outside of ObservableCollection
. Still, from the level of the abstraction/interface, you really want Completed
to be a property of that Tasks
collection.
For this, I think a better approach would be to create "sub" view-model for your Tasks
property:
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");
}
This gives you all of the benefits of the ObservableCollection
, and effectively makes the Completed
property part of it. We still haven't captured only the cases where the number of completed items truly changes, but we have reduced the number of redundant notifications somewhat.
Now the viewmodel just has the property:
public TasksViewModel Tasks { get; set; }
…and you can bind to Tasks
, Tasks.Count
, and Tasks.Completed
with ease.
As an alternative, if you would rather create these other properties on the "main" view-model, you can take this notion of a subclassed ObservableCollection<T>
to create one with some method where you can pass in an Action<string>
delegate, which would represent raising a property change notification on the main view-model, and some list of property names. This collection could then effectively raise the property change notifications on the view-model:
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);
}
}
You could even leave the property type as 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));
}
}
Looks rather elegant to me. I really don't know how you'd make that more succinct.
(How odd to write an answer like this. If somebody actually comes up with something more elegant, I might delete this.)
Okay, I noticed one thing, unrelated to the original question: Your Tasks
property has a public setter. Make it private set;
, or you'll need to implement the set
with a backing field so you can remove the delegate on the previous instance, replace and wire up the new one, and do OnPropertyChanged
with "Tasks", "Count", and "Completed". (And seeing how Tasks
is set in the constructor, I'm guessing private set;
is the better option.)
Doesn't make notifying about Count
and Completed
more elegant, but it fixes a bug.
And many MVVM frameworks get the property name from a lambda, so that instead of OnPropertyChanged("Count")
, you can write OnPropertyChanged(() => Count)
so that it will follow renames done with the help of refactoring tools. I don't think renaming happens all that often, though, but it does avoid some string literals.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With