15

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)

H.B.
  • 166,899
  • 29
  • 327
  • 400
  • 1
    You know more about this stuff than I do, so I'm curious - outside of DP's and INPC's what events or actions cause the binding system to re-evaluate all the bindings currently in the UI? Are there any? If so, can any of them be relied upon to occur reliably? – Tim May 25 '11 at 12:11
  • @Tim: Cannot think of any, well, when the DataContext changed that causes updates (for the bindings which bind to it) in the subtree due to inheritance. In WPF you have public access to the associated [`DataContextChanged`](http://msdn.microsoft.com/en-us/library/system.windows.frameworkelement.datacontextchanged.aspx) event. In Silverlight it's internal for some reason. – H.B. May 25 '11 at 12:16
  • It might be worth stating as a WPF design requirement that all properties have setters, and that the getter always returns the set value. Otherwise saying functions like the one above represents the GUI is essentially a lie. – Dax Fohl Sep 26 '12 at 14:24
  • @DaxFohl: I do not think that this is a good idea, you would need to introduce redundant backing fields for something that can be retrieved on the fly. Also the property then would lie about its contents because it cannot be accurate as it can only be refreshed every so often. – H.B. Sep 26 '12 at 14:50
  • Your question is general and fundamental. The time is continuous. The UI updates are strictly discrete. So, you need to update your UI from a continuous data source at discrete points. So, the pumping is inevitable. You can use a timer (you can also subscribe to the `CompositionTarget.Rendering` event, but I don't think that it's called for) I'm making a library which allows such bindings (along with other things), but your case is rather simple and won't benefit from it. – Ark-kun Dec 23 '12 at 00:23

3 Answers3

4

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);
default.kramer
  • 5,943
  • 2
  • 32
  • 50
  • 1
    Nice effort, a few things though: You don't need a dispatcher timer for this since you don't access thread affine objects, further the encapsulation might have the downside that adjusting the countdown becomes more complex when the Deadline is changed since the property is no longer directly dependent on it as in my initial code. Also the construction of the class seems a bit confusing to me but that probably could be fixed with some documentation on the constructors. – H.B. May 25 '11 at 15:49
  • Good point about the dispatcher timer - I was thinking of making a View and doing a "manual binding", so I guess I just had dispatcher timers on the mind. I didn't consider updating the Deadline - this makes the first constructor useless, but you could still use the 2nd one. – default.kramer May 25 '11 at 16:00
  • Possibly making the first one take Deadline using `ref` might work as well, no? – H.B. May 25 '11 at 16:11
  • 2
    I don't believe so, but I've been wrong before :). You can't [use it in the lambda](http://stackoverflow.com/questions/4236103/c-cannot-use-ref-or-out-parameter-inside-an-anonymous-method-body) and you can't hold on to the "refness" using a field. – default.kramer May 25 '11 at 16:40
3

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.

Tim
  • 14,999
  • 1
  • 45
  • 68
  • 2
    I also thought that using a Timer might be the best approach, one improvement might be to only use one static timer for all tasks and to make the timer sleep if the application is minimized. – H.B. May 25 '11 at 15:37
  • Yeah, there's definitely some optimizations that could be made. Both of those are good suggestions. – Tim May 25 '11 at 15:46
  • 1
    *Accpeted until a magician stops by and offers something amazingly superior :P* – H.B. Jun 15 '11 at 22:32
1

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.

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286