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));
}
}
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 :) )