Binding to time-dependent properties

2019-01-18 21:16发布

问题:

Some time ago i wrote a small widget-like application which was supposed to keep track of tasks, each task had a deadline specified as a DateTime, now if you want to display how much time is left until the deadline you might want to bind to a "virtual" (*curses the virtual keyword*) property like this:

public TimeSpan TimeLeft
{
    get { return Deadline - DateTime.Now; }
}

Obviously in theory this property changes every tick and you want to update your UI every now and then (e.g. by periodically pumping out a PropertyChanged event for that property).

Back when i wrote the widget i refreshed the whole task list every minute, but this is hardly ideal since if the user interacts with some item (e.g. by typing in a TextBox which binds to a Comments-property) that will be harshly interupted and updates to the source get lost.

So what might be the best approach to updating the UI if you have time-dependent properties like this?

(I don't use that application anymore by the way, just thought this was a very interesting question)

回答1:

I think what you said in your first paragraph after the code sample is the only reasonable way to make this work in WPF. Set up a timer that Just calls PropertyChanged for the TimeLeft property. The interval would vary based upon your scenario (if you're talking a weekly task list, you probably only need to update it ever 5 minutes or so. If you're talking a task list for the next 30 minutes, you may need to update it every minute or 30 seconds or something.

That method would avoid the problems you mentioned with the refresh option since only the TimeLeft bindings would be affected. If you had millions of these tasks, I guess the performance penalty would be pretty significant. But if you only had a few dozen or something, updating those bindings every 30 seconds or so would be be a pretty insignificant issue, right?

Every possibility that I can think of uses either Timers or Animations. The animations would be way too "heavy" as you add tasks to the list. And of the Timer scenarios, the one above seems to be the cleanest, simplest and most practical. Probably just comes down to whether it even works or not for your specific scenario.



回答2:

A timer is the only way I can think of. Since this is an interesting question, I'll put my .02 in. I would encapsulate it doing something like this:

public class CountdownViewModel : INotifyPropertyChanged
{
    Func<TimeSpan> calc;
    DispatcherTimer timer;

    public CountdownViewModel(DateTime deadline)
        : this(() => deadline - DateTime.Now)
    {
    }

    public CountdownViewModel(Func<TimeSpan> calculator)
    {
        calc = calculator;

        timer = new DispatcherTimer();
        timer.Interval = TimeSpan.FromSeconds(1);
        timer.Tick += timer_Tick;
        timer.Start();
    }

    void timer_Tick(object sender, EventArgs e)
    {
        var temp = PropertyChanged;
        if (temp != null)
        {
            temp(this, new PropertyChangedEventArgs("CurrentValue"));
        }
    }

    public TimeSpan CurrentValue
    {
        get
        {
            var result = calc();
            if (result < TimeSpan.Zero)
            {
                return TimeSpan.Zero;
            }
            return result;
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

public class MyViewModel
{
    public CountdownViewModel DeadlineCountdown { get; private set; }

    public DateTime Deadline { get; set; }

    public MyViewModel()
    {
        Deadline = DateTime.Now.AddSeconds(200);
        DeadlineCountdown = new CountdownViewModel(Deadline);
    }
}

Then you could bind to DeadlineCountdown.CurrentValue directly, or create a CountdownView. You could move the timer to the CountdownView, if you wanted. You could use a static timer so they all update at the same time.

Edit

If Deadline is going to change, you would have to construct the countdown like this:

DeadlineCountdown = new CountdownViewModel(() => this.Deadline - DateTime.Now);


回答3:

I read your accepted answer, but I was just wondering... why not just disable the bindings for that specific task while in 'Edit' mode so you wouldn't be interrupted? Then simply re-enable that binding when you're either done, or you cancel your edit? That way even if your timer updated every second, who cares?

As for how to disable them without detaching them (and thus resetting their value), simply define a boolean flag, then in all the DPs that you want to interrupt, check for that flag in the validation logic. If the flag is true and the DependencyObject that it applies to is the one you're editing, block the change to the DP.

Anyway, this just popped into my head. Haven't actually tested it but it should be an easy thing to try.