Ace McCloud
Ace McCloud

Reputation: 900

subscribing to different events on Property changed if different properties

I have class Step which has a collection of Task i.e List . Step has properties Status , Time . Task also has the same properties. The values of Status and Time for Step need to be updated whenver anyone of the Tasks get their Time or Status changed. For this , I am adding handlers to each task in the Step class.

 private void AddHandlers()
        {
            foreach (Task tsk in Tasks)
            {
                tsk.PropertyChanged += HandleStatusChanged;

                tsk.PropertyChanged += HandleTimeChanged;
            }
        }
    private void HandleStatusChanged(object sender, EventArgs e)
        {
            UpdateStepStatusFromTasks();

        }
        private void HandleTimeChanged(object sender, EventArgs e)
        {
            UpdateStepTimesFromTasks();

        }

 private void UpdateStepTimesFromTasks()
        {
        // logic for calculating Time for Step

        }

        private void UpdateStepStatusFromTasks()
        {

// logic for calculating Status for Step

        }

Here is the Property changed event handler in Task public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        var handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));

    }

My issue is that even if I change only Task Time , it calls both the handlers Status and time as they are subscribed to the same property changed event on task.

How can i bifurcate the Property changed event based on Property called from and ensure that only the respective handlers get called and not both together ?

Sorry if this sounds silly , but I am somewhat a beginner to WPF.

Regards, P

Upvotes: 1

Views: 1954

Answers (3)

Enigmativity
Enigmativity

Reputation: 117064

So, the obvious thing here is that you are attaching two handlers to the `` event so everything is being processed twice. It needs be only subscribed to once.

But rather than making a lot of complicated methods with code bouncing around all over the place, I prefer to using Microsoft's Reactive Extensions (Rx) - NuGet "Rx-Main" - to do anything with events. After learning a few basic operators it really makes working with events much much easier.

Rx is, in overly simplistic terms, LINQ for Events. It lets you work with queries to handle events rather than enumerables. It creates observables.

First, I would create this observable:

var tpns = // IObservable<{anonymous}>
    from t in Tasks.ToObservable()
    from ep in Observable.FromEventPattern<
            PropertyChangedEventHandler, PropertyChangedEventArgs>(
        h => t.PropertyChanged += h,
        h => t.PropertyChanged -= h)
    select new { Task = t, ep.EventArgs.PropertyName };

This query basically takes the list of Tasks and converts all of the PropertyChanged events of each task in a single observable that returns each Task when that task had a property change and the PropertyName of the task that changed.

Now it's easy to create a couple more observables that filter by PropertyName and return the Task:

IObservable<Task> statusChanges =
    from tpn in tpns
    where tpn.PropertyName == "Status"
    select tpn.Task;

IObservable<Task> timeChanges =
    from tpn in tpns
    where tpn.PropertyName == "Time"
    select tpn.Task;

Those should be really simple to understand.

Now subscribe to each (basically like attaching to events):

IDisposable statusSubscription =
    statusChanges
        .Subscribe(task => UpdateStepStatusFromTasks());

IDisposable timeSubscription =
    timeChanges
        .Subscribe(task => UpdateStepTimesFromTasks());

You'll notice each subscription is an IDisposable. Instead of detaching from events using the -= operator you simply call .Dispose() on the subscription and all of the underlying event handlers are detached for you.

Now I would recommend changing the AddHandlers method to return an IDisposable. Then the code that calls AddHandlers can dispose of the handlers - if needed - to make sure you can clean up before exiting.

So the complete code would look like this:

private IDisposable AddHandlers()
{
    var tpns = // IObservable<{anonymous}>
        from t in Tasks.ToObservable()
        from ep in Observable.FromEventPattern<
                PropertyChangedEventHandler, PropertyChangedEventArgs>(
            h => t.PropertyChanged += h,
            h => t.PropertyChanged -= h)
        select new { Task = t, ep.EventArgs.PropertyName };

    IObservable<Task> statusChanges =
        from tpn in tpns
        where tpn.PropertyName == "Status"
        select tpn.Task;

    IObservable<Task> timeChanges =
        from tpn in tpns
        where tpn.PropertyName == "Time"
        select tpn.Task;

    IDisposable statusSubscription =
        statusChanges
            .Subscribe(task => UpdateStepStatusFromTasks());

    IDisposable timeSubscription =
        timeChanges
            .Subscribe(task => UpdateStepTimesFromTasks());

    return new CompositeDisposable(statusSubscription, timeSubscription);
}

The only new thing there is the CompositeDisposable which joins the two IDiposable subscriptions into a single IDisposable.

The very nice thing about this approach is that most of the code now sits nicely in a single method. It makes it easy to understand and maintain when done this way - at least after a small learning curve. :-)

Upvotes: -1

user853710
user853710

Reputation: 1767

Every event has "accessors" add or remove. Something similar like get/set for properties. This accessors can show you the nature of the event. Every event has an InvocationList, which represents a collection of object that it will notify when the event is raised. Using this accessors you can you can have more control over what get notified and what not. When you subscribe to the event, the subscribed object get inserted into the Invocation list.

Since you are subscribing the same object for both events, you will have it triggered twice.

Only thing you can do is to check the name of the property that got updated

public void ChangedHandler(object sender, PropertyChangedEventArgs  e)
{
    if(e.PropertyName=="Time"){//do something}
    else if (e.PropertyName == "Date") {doSomething}
}

Since you are dealing with WPF, I see a strange pattern here. You are raising the events from various methods. You should be raising the event from a property for which you want the notification to happen, which is bound to a control.

public class MyVM
{
    private string _status = "status1";
    public string Status
    {
        get
        {
            return _status;
        }
        set
        {
            if(_status!=value)
            {
                _status =value
                OnPropertyChanged("Status");
            }
        }
    }
}

You can improve on this using various things like "nameof", baseClasses, or MethorVeawers like FODY

Upvotes: 3

Scott Chamberlain
Scott Chamberlain

Reputation: 127563

You need to check the parameter of the args that are passed in to get the name of the property.

First get rid of your double subscription.

private void AddHandlers()
{
    foreach (Task tsk in Tasks)
    {
        tsk.PropertyChanged += HandlePropertyChanged;
    }
}

Then use the correct signature for your event so you get the correct type of event args.

private void HandlePropertyChanged(object sender, PropertyChangedEventArgs e)
{

Now that we have PropertyChangedEventArgs instead of just EventArgs we can check the PropertyName property and call the needed method.

private void HandlePropertyChanged(object sender, PropertyChangedEventArgs e)
{
    switch(e.PropertyName)
    {
        case "Status":
            UpdateStepStatusFromTasks();
            break;
        case "Time":
            UpdateStepTimesFromTasks();
            break;
     }
}

As you need more properties handled you can just add them to the switch statement.


P.S. Instead of manually subscribing to each Task you can use a BindingList<Task> as the collection that holds the tasks, you can then subscribe to the ListChanged event, that event will be raised if any of the items in the list raise PropertyChanged (be sure to enable RaiseListChangedEvents and check ListChangedEventArgs.ListChangedType is equal to ListChangedType.ItemChanged).

Upvotes: 3

Related Questions