Search code examples
c#xamldata-bindingwin-universal-appcountdown

UWP multiple Countdowns with C# and XAML


I am building an UWP app with C# and XAML. On a custom user control I build a "list" better a ListView with a DataTemplate and a grid inside. In this grid I have multiple TextBlocks with DataBindings. In each generated row I now want a Countdown Timer from DateTimeOffset.Now to a specific DateTimeOffset in the future (or past - negative values).

I tried to build a class CountDownElement with a text property and called my "countdown" Class with that string. In Xaml I did a DataBinding in a TextBlock to that string. But it did not change. I think tghe UI doesn't get updated? Is there a different way - maybe to build the countdown only with XAML and bind its value to the a generated value of the ViewModel?

Thanks for your help!

Here my CountdownClass:

 public class Countdown
{
    public DispatcherTimer DispatcherTimer;
    public DateTimeOffset StartTime;
    private TimeSpan _time;
    private string _tb;
    private readonly DateTimeOffset _endTime;

    public Countdown(string tb, DateTimeOffset endTime)
    {
        _tb = tb;
        _endTime = endTime;
        DispatcherTimerSetup();
    }

    private void DispatcherTimerSetup()
    {
        DispatcherTimer = new DispatcherTimer();
        StartTime = DateTimeOffset.Now;
        _time = new TimeSpan();
        _time = _endTime - StartTime;

        DispatcherTimer.Interval = new TimeSpan(0, 0, 0, 1);
        DispatcherTimer.Tick += dispatcherTimer_Tick;

        DispatcherTimer.Start();

    }

    private void dispatcherTimer_Tick(object sender, object e)
    {
        _time = _time.Subtract(new TimeSpan(0, 0, 1));
        if (_time <= TimeSpan.Zero)
        {
            _tb = "- " + _time.ToString(@"dd\:hh\:mm\:ss");

        }
        else
        {
            _tb = "  " + _time.ToString(@"dd\:hh\:mm\:ss");
        }
    }
}

Here is my Countdown Element where I create a new Countdown Object:

 public class ListCountdownElement
{
    public string CountdownElement;

    public ListCountdownElement(Incident incident, int type)
    {
        CountdownElement = "Countdown";

        switch (type)
        {
            case 1:
                if (incident.Resolvebykpiid != null)
                {
                    var endTime = incident.Resolvebykpiid.Failuretime;
                    if (endTime != null)
                    {
                        var c = new Countdown(CountdownElement, (DateTimeOffset)endTime);
                    }
                }
                break;
            case 2:
                if (incident.Firstresponsebykpiid != null)
                {
                    if (incident.Firstresponsesent != null && incident.Firstresponsesent.Value)
                    {
                        CountdownElement.Foreground = new SolidColorBrush(Colors.Green);
                        CountdownElement.Text = "sent";
                    }
                    else
                    {
                        var endTime = incident.Firstresponsebykpiid.Failuretime;
                        if (endTime != null)
                        {
                            var c = new Countdown(CountdownElement, (DateTimeOffset)endTime);
                        }
                    }

                }
                break;
            case 3:
                if (incident.Resolvebykpiid != null)
                {
                    var endTime = incident.Resolvebykpiid.Failuretime;
                    if (endTime != null)
                    {
                        var c = new Countdown(CountdownElement, (DateTimeOffset)endTime);
                    }
                }
                break;
            default:
                break;
        }
    }
}

This is the IncidentViewModel, where I create the Countdown Element - this is the data, I bind to.

 public class IncidentViewModel
{
    public string Id { get; set; }
    public string Title { get; set; }
    public string Customer { get; set; }
    public ListStatusBar StatusBar { get; set; }
    public ListCountdownElement Countdown { get; set; }
    public ListStatusLight Status { get; set; }

    public IncidentViewModel(Incident incident, IncidentTypes type)
    {
        this.Id = incident.Ticketnumber;
        this.Title = incident.Title;
        this.Customer = incident.Customerid_account.Name;
        this.StatusBar = new ListStatusBar(incident, type);
        this.Countdown = new ListCountdownElement(incident, type.GetHashCode());
        this.Status = new ListStatusLight(incident, type);
    }
}

This is the ListViewModel, where I fetch my Data and create the incidentViewModels and put them to an observable collection:

public class IncidentListViewModel
{

    public ObservableCollection<Incident> Incidents;
    public ObservableCollection<IncidentViewModel> IncidentsCollection;

    public IncidentListViewModel()
    {
        IncidentsCollection = new ObservableCollection<IncidentViewModel>();
    }
    public async Task<ObservableCollection<IncidentViewModel>> GetData(string accessToken, IncidentTypes type)
    {
        Incidents = await DataLoader.LoadIncidentsData(accessToken, type.GetHashCode());
        IncidentsCollection.Clear();
        foreach (var incident in Incidents)
        {
            var incidentVm = new IncidentViewModel(incident, type);

            IncidentsCollection.Add(incidentVm);
        }

        return IncidentsCollection;
    }
}

This is the xaml.cs of the listcontrol, where I bind to the observable collection:

public sealed partial class IncidentListControl : UserControl
{
    private readonly IncidentListViewModel _incidentListVm;
    private readonly string _accessToken;
    private readonly IncidentTypes _type;
    public IncidentListControl(string accessToken, IncidentTypes type)
    {
        this.InitializeComponent();
        this._accessToken = accessToken;
        this._type = type;
        this.ListHeader.Text = type.ToString();
        _incidentListVm = new IncidentListViewModel();
        GetIncidentData();

        //Get Incident Data and update UI
        var period = TimeSpan.FromSeconds(60);
        var periodicTimer = ThreadPoolTimer.CreatePeriodicTimer(async (source) =>
        {
            await Dispatcher.RunAsync(CoreDispatcherPriority.High,
                GetIncidentData);
        }, period);
    }

    private async void GetIncidentData()
    {
        await _incidentListVm.GetData(_accessToken, _type);
        this.ListViewIncidents.ItemsSource = _incidentListVm.IncidentsCollection;
    }
}

And finally the xaml, where I do the binding on the TextBlocks:

<ListView Name="ListViewIncidents" Grid.Row="1">
        <ListView.ItemTemplate>
            <DataTemplate>
                <Grid Margin="5">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="150"/>
                        <ColumnDefinition Width="200"/>
                        <ColumnDefinition Width="200"/>
                        <ColumnDefinition Width="100"/>
                        <ColumnDefinition Width="100"/>
                        <ColumnDefinition Width="*"/>
                    </Grid.ColumnDefinitions>
                    <TextBlock Text="{Binding Id}" Margin="3" Grid.Column="0" FontWeight="Bold" FontSize="12" TextWrapping="Wrap" />
                    <TextBlock Text="{Binding Title}" Margin="3" Grid.Column="1" FontWeight="Bold" FontSize="12" TextWrapping="Wrap" />
                    <TextBlock Text="{Binding Customer}" Margin="3" Grid.Column="2" FontWeight="Bold" FontSize="12" TextWrapping="Wrap" />
                    <Border Margin="3" Grid.Column="3" Height="10" BorderThickness="1" BorderBrush="Black">
                        <Rectangle  Fill="{Binding StatusBar.Color}" Width="{Binding StatusBar.Width}" HorizontalAlignment="Left" Height="10"></Rectangle>
                    </Border>
                   <TextBlock Text="{Binding Countdown.CountdownElement}" Margin="3" Grid.Column="4" FontWeight="Bold" FontSize="12" TextWrapping="Wrap" />
                    <Ellipse Margin="3" Height="10" Width="10" Stroke="Black" Fill="{Binding Status.StatusColor}" Grid.Column="5"/>
                </Grid>

            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>

This is just the relevant part. I hope this update helps!

Updated Code: Countdown.cs

public class Countdown : INotifyPropertyChanged
{
    public DispatcherTimer DispatcherTimer;
    public DateTimeOffset StartTime;
    private TimeSpan _time;
    private string _tb;
    public string Tb
    {
        get { return _tb; }
        set
        {
            if (Equals(value, _tb))
                return;

            _tb = value;
            OnPropertyChanged();
        }
    }
    private readonly DateTimeOffset _endTime;

    public Countdown( DateTimeOffset endTime)
    {
      //  _tb = tb;
        _endTime = endTime;
        DispatcherTimerSetup();
    }

    private void DispatcherTimerSetup()
    {
        DispatcherTimer = new DispatcherTimer();
        StartTime = DateTimeOffset.Now;
        _time = new TimeSpan();
        _time = _endTime - StartTime;

        DispatcherTimer.Interval = new TimeSpan(0, 0, 0, 1);
        DispatcherTimer.Tick += dispatcherTimer_Tick;

        DispatcherTimer.Start();

    }

    private void dispatcherTimer_Tick(object sender, object e)
    {
        _time = _time.Subtract(new TimeSpan(0, 0, 1));
        if (_time <= TimeSpan.Zero)
        {
            Tb = "- " + _time.ToString(@"dd\:hh\:mm\:ss");
        }
        else
        {
            Tb = "  " + _time.ToString(@"dd\:hh\:mm\:ss");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

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

ListCountdownElement.cs

 public class ListCountdownElement 
{
    private Countdown _countDown;
    public string CountdownElement
    {
        get {return _countDown.Tb ; }          
    }


    public ListCountdownElement(Incident incident, int type)
    {

        switch (type)
        {
            case 1:
                if (incident.Resolvebykpiid != null)
                {
                    var endTime = incident.Resolvebykpiid.Failuretime;
                    if (endTime != null)
                    {
                         _countDown = new Countdown( (DateTimeOffset)endTime);
                    }
                }
                break;
            case 2:
                if (incident.Firstresponsebykpiid != null)
                {
                    if (incident.Firstresponsesent != null && incident.Firstresponsesent.Value)
                    {
                        //CountdownElement = "sent"; --> think about that later
                    }
                    else
                    {
                        var endTime = incident.Firstresponsebykpiid.Failuretime;
                        if (endTime != null)
                        {
                            _countDown = new Countdown( (DateTimeOffset)endTime);
                        }
                    }

                }
                break;
            case 3:
                if (incident.Resolvebykpiid != null)
                {
                    var endTime = incident.Resolvebykpiid.Failuretime;
                    if (endTime != null)
                    {
                         _countDown = new Countdown((DateTimeOffset)endTime);
                    }
                }
                break;
            default:
                break;
        }
    }
}

IncidentViewModel.cs

public class IncidentViewModel
{
    public string Id { get; set; }
    public string Title { get; set; }
    public string Customer { get; set; }
    public ListStatusBar StatusBar { get; set; }
    public ListCountdownElement Countdown { get; set; }
    public ListStatusLight Status { get; set; }

    public IncidentViewModel(Incident incident, IncidentTypes type)
    {
        this.Id = incident.Ticketnumber;
        this.Title = incident.Title;
        this.Customer = incident.Customerid_account.Name;
        this.StatusBar = new ListStatusBar(incident, type);
        this.Countdown = new ListCountdownElement(incident, type.GetHashCode());
        this.Status = new ListStatusLight(incident, type);
    }
}

IncidentListControl.xaml.cs, IncidentListControl.xaml.cs and IncidentListViewModel.cs are untouched

EDIT: Final Version new ListCountdownElement.cs:

public class ListCountdownElement : INotifyPropertyChanged
{
    public DispatcherTimer DispatcherTimer;
    public DateTimeOffset StartTime;
    private TimeSpan _time;
    private DateTimeOffset _endTime;
    private string _countdownElement;
    public string CountdownElement
    {
        get { return _countdownElement; }
        set
        {
            if (Equals(value, _countdownElement))
                return;

            _countdownElement = value;
            OnPropertyChanged();
        }
    }

    public ListCountdownElement(Incident incident, int type)
    {
        switch (type)
        {
            case 1:
                if (incident.Resolvebykpiid != null)
                {
                    _endTime = (DateTimeOffset)incident.Resolvebykpiid.Failuretime;
                    DispatcherTimerSetup();
                }
                break;
            case 2:
                if (incident.Firstresponsebykpiid != null)
                {
                    if (incident.Firstresponsesent != null && incident.Firstresponsesent.Value)
                    {
                        //CountdownElement = "sent"; --> think about that later
                    }
                    else
                    {
                        _endTime = (DateTimeOffset)incident.Firstresponsebykpiid.Failuretime;
                        DispatcherTimerSetup();
                    }

                }
                break;
            case 3:
                if (incident.Resolvebykpiid != null)
                {
                    _endTime = (DateTimeOffset)incident.Resolvebykpiid.Failuretime;
                    DispatcherTimerSetup();
                }
                break;
            default:
                break;
        }

    }
    private void DispatcherTimerSetup()
    {
        DispatcherTimer = new DispatcherTimer();
        StartTime = DateTimeOffset.Now;
        _time = new TimeSpan();
        _time = _endTime - StartTime;

        DispatcherTimer.Interval = new TimeSpan(0, 0, 0, 1);
        DispatcherTimer.Tick += dispatcherTimer_Tick;

        DispatcherTimer.Start();

    }

    private void dispatcherTimer_Tick(object sender, object e)
    {
        _time = _time.Subtract(new TimeSpan(0, 0, 1));
        if (_time <= TimeSpan.Zero)
        {
            CountdownElement = "- " + _time.ToString(@"dd\:hh\:mm\:ss");
        }
        else
        {
            CountdownElement = "  " + _time.ToString(@"dd\:hh\:mm\:ss");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

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

Solution

  • So it seems you are not triggering the correct event to let the ui know that the text has changed. The code you are showing also look a bit complex for what you are trying to accomplish...

    First make sure that the CountDown class inherits INotifyPropertyChanged. When you add that interface you'll also need to add following code to that class:

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

    After that, change the _tb field to an actual property, like this:

    private string _tb;
    public string Tb
    {
        get { return _tb; }
        set
        {
            if(Equals(value, _tb))
                return;
    
            _tb = value;
            OnPropertyChanged();
        }
    }
    

    **** Important ****

    In the dispatcherTimer_Tick event, change the value of Tb ( the property ), NOT _tb the field.

    Now you'll need to route that property to the UI, so in the ListCountdownElement class, change the CountdownElement field also to a real property.

    public string CountdownElement
    { get { return _countDown.Tb; } }
    

    Only thing to do is, add a _countDown field of type CountDown and be sure to assign that in your ListCountdownElement constructor.

    I guess that should fix it... ( not tested though :) )