New DispatcherTimer gets created alongside old one, only new one should run - c#

I ran into a problem the other day. I have found out why it's happening, but I've never had a run in with such a problem so I don't know how to solve it.
I have an application where in the DashboardView (the main view) a DispatcherTimer is started in the DashboardViewModel. When the Timer ticks, we get the data from the database, this list is databound between the View and the ViewModel. When there is new data that caused the database to change, a sound will play.
The user can go to other Views. When the user goes back to the DashboardView, the DashboardViewModel is again created and so is the DispatcherTimer.
Now there are 2 Timers and they both fire the Tick event, creating a confusing scenario for the user.
This is my observation of what happens in the application right now:
My Timer ticks every minute. When I start the application, DashboardView #1 opens. DashboardViewModel #1 starts and so does DispatcherTimer #1.
I switch to a different view, and make an update to the data (a new email) so when the Timer ticks, the list in the DashboardView will change and a sound is played.
When Timer #1 is at 30 seconds, I switch to the DashboardView, which is newly created thus creating View&ViewModel&Timer #2.
After 1 minute, Timer #1 ticks, there is new data so it updates the DB and plays a sound, yet the list in the View doesn't update.
I think that this is because View #2 is showing over #1. I know because otherwise I would see an overlay saying it's refreshing.
View #2 is databound to ViewModel #2. Timer #1 updated ViewModel #1, so the changes won't show as we can't see View #1 as it's replaced/overlapped by View #2.
After 1 min 30 seconds, Timer #2 ticks, gets the data from the DB, doesn't play a sound as the DB was already brought up-to-date by Timer #1, and shows the data in the new state.
(I hope that made sense)
So, TLDR: There are 2 Timers running while only 1 should be active (the newest one, I think).
How can I achieve this?
Here's (part of) the DashboardViewModel as I have it now:
namespace QRM.ViewModel
{
class DashboardListViewModel : INotifyPropertyChanged
{
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
DBServer dbServer = new DBServer();
#region Constructor
public DashboardListViewModel()
{
log.Info("Dashboard Initializing - Starting...");
MyObservableCollection<View_server> listDashboard = new MyObservableCollection<View_server>();
ListDashboard = dbServer.ReadDashboard();
listBoxCommand = new RelayCommand(() => SelectionHasChanged());
// Refresh to get all new emails, errors, etc.
GetListDashboard();
IsRefreshing = Visibility.Collapsed;
// Make a timer to renew the data in the Dashboard automatically.
DispatcherTimer timer = new DispatcherTimer();
timer.Tick += new EventHandler(timer_Tick);
timer.Interval = Properties.Settings.Default.Timer_interval; // hours, minutes, seconds.
timer.Start();
//Receive the Notification sent after DashboardDetailsViewModel has handled the button commands, and call a respond method for the List.
App.Messenger.Register("RefreshServers", (Action)(() => GetListDashboard()));
App.Messenger.Register("ClearSelection", (Action)(() => SelectedServer = null));
App.Messenger.Register("ErrorSolved", (Action)(() => KeepSelection(selectedServer)));
App.Messenger.Register("WarningSound", (Action)(() => HasNewError = true));
log.Info("Dashboard Initializing - Done.");
}
#endregion
#region Get list dashboard
private void GetListDashboard()
{
HasNewError = false;
log.Info("Dashboard - Checking for Email...");
// The old Outlook class and methods
//EmailManager checkMail = new EmailManager();
//checkMail.GetEmail();
// First, check for mail.
IMAPManager checkMail = new IMAPManager();
checkMail.GetEmail();
log.Info("Dashboard - Checking for linked Errors...");
// Check if the emails have Errors linked to them. If not, add the Error from the Email to the DB
ErrorManager checkError = new ErrorManager();
checkError.GetNewErrors();
log.Info("Dashboard List - Starting...");
// Load the dashboard.
ListDashboard = dbServer.ReadDashboard();
System.Diagnostics.Debug.WriteLine("REFRESHED THE DASHBOARD");
log.Info("Dashboard List - Done.");
}
private void KeepSelection(View_server keepSelection)
{
GetListDashboard();
SelectedServer = keepSelection;
SelectionHasChanged();
}
#endregion
#region Timer
//This method runs every time the timer ticks.
private async void timer_Tick(object sender, EventArgs e)
{
log.Info("Dashboard - Refreshing...");
System.Diagnostics.Debug.WriteLine(">>Timer tick");
IsRefreshing = Visibility.Visible;
// To make sure the overlay is visible to the user, let it be on screen for at least a second (2x half a second)
await Task.Delay(500);
if (selectedServer != null)
{
KeepSelection(selectedServer);
}
else
{
GetListDashboard();
}
// 2nd half second.
await Task.Delay(500);
IsRefreshing = Visibility.Collapsed;
if (hasNewError == true)
{
System.Diagnostics.Debug.WriteLine("List has new error");
PlayWarningSound();
HasNewError = false;
}
else
{
System.Diagnostics.Debug.WriteLine("List has no new error");
HasNewError = false;
}
System.Diagnostics.Debug.WriteLine(">>End timer");
log.Info("Dashboard - Refreshed.");
}
#endregion
}
}

There are a few issues going on here. Let's start with the most basic first:
Cleanup
When the DashboardListViewModel is disposed of or closed, you need to unwire your DispatcherTimer.Tick event handler, invoke .Stop() and then call .Finalize(). MSDN. This will ensure that your System.Windows.Threading.DispatcherTimer is properly cleaned up.
Async / Await & Event Handlers
Also, the DispatcherTimer.Tick event handler is defined as async void. This is the incorrect usage of the async keyword. Instead use this:
private void timer_Tick(object sender, EventArgs e)
{
log.Info("Dashboard - Refreshing...");
System.Diagnostics.Debug.WriteLine(">>Timer tick");
IsRefreshing = Visibility.Visible;
// To make sure the overlay is visible to the user, let it be on screen for at least a second (2x half a second)
Thread.Sleep(500);
if (selectedServer != null)
{
KeepSelection(selectedServer);
}
else
{
GetListDashboard();
}
// 2nd half second.
Thread.Sleep(500);
IsRefreshing = Visibility.Collapsed;
if (hasNewError == true)
{
System.Diagnostics.Debug.WriteLine("List has new error");
PlayWarningSound();
HasNewError = false;
}
else
{
System.Diagnostics.Debug.WriteLine("List has no new error");
HasNewError = false;
}
System.Diagnostics.Debug.WriteLine(">>End timer");
log.Info("Dashboard - Refreshed.");
}
I usually never advise using Thread.Sleep but since you're already in the context of a Threading Timer this makes sense.
One last concern
Are you certain that the App.Messenger.Register can be invoked multiple times, as it occurs every time your view model is instantiated? I would have imagined this would be something you'd only ever want to do once, in static context.

Related

How to keep cancelling the task until a condition is met (TaskCanceledException)

I want to call a method after some delay when an event is raised, but any subsequent events should "restart" this delay. Quick example to illustrate, the view should be updated when scrollbar position changes, but only 1 second after the user has finished scrolling.
Now I can see many ways of implementing that, but the most intuitive would be to use Task.Delay + ContinueWith + cancellation token. However, I am experiencing some issues, more precisely subsequent calls to my function cause the TaskCanceledException exception and I started to wonder how I could get rid of that. Here is my code:
private CancellationTokenSource? _cts;
private async void Update()
{
_cts?.Cancel();
_cts = new();
await Task.Delay(TimeSpan.FromSeconds(1), _cts.Token)
.ContinueWith(o => Debug.WriteLine("Update now!"),
TaskContinuationOptions.OnlyOnRanToCompletion);
}
I have found a workaround that works pretty nicely, but I would like to make the first idea work.
private CancellationTokenSource? _cts;
private CancellationTokenRegistration? _cancellationTokenRegistration;
private void Update()
{
_cancellationTokenRegistration?.Unregister();
_cts = new();
_cancellationTokenRegistration = _cts.Token.Register(() => Debug.WriteLine("Update now!"));
_cts.CancelAfter(1000);
}
You should consider using Microsoft's Reactive Framework (aka Rx) - NuGet System.Reactive and add using System.Reactive.Linq;.
You didn't say hat UI you're using, so for Windows Forms also add System.Reactive.Windows.Forms and for WPF System.Reactive.Windows.Threading.
Then you can do this:
Panel panel = new Panel(); // assuming this is a scrollable control
IObservable<EventPattern<ScrollEventArgs>> query =
Observable
.FromEventPattern<ScrollEventHandler, ScrollEventArgs>(
h => panel.Scroll += h,
h => panel.Scroll -= h)
.Select(sea => Observable.Timer(TimeSpan.FromSeconds(1.0)).Select(_ => sea))
.Switch();
IDisposable subscription = query.Subscribe(sea => Console.WriteLine("Hello"));
The query is firing for every Scroll event and starts a one second timer. The Switch operator watches for every Timer produces and only connects to the latest one produced, thus ignoring the previous Scroll events.
And that's it.
After scrolling has a 1 second pause the word "Hello" is written to the console. If you begin scrolling again then after every further 1 second pause it fires again.
In my own experience I've dealt with lots of scenarios just like the one you describe, e.g. update something one second after the mouse stops moving etc.
For a long time I would do timer restarts just the way you describe, by cancelling an old task and starting a new one. But I never really liked how messy that was, so I came up with an alternative that I use in production code. Long-term it has proven quite reliable. It takes advantage of the captured context associated with a task. Multiple instances of TaskCanceledException no longer occur.
class WatchDogTimer
{
int _wdtCount = 0;
public TimeSpan Interval { get; set; } = TimeSpan.FromSeconds(1);
public void Restart(Action onRanToCompletion)
{
_wdtCount++;
var capturedCount = _wdtCount;
Task
.Delay(Interval)
.GetAwaiter()
.OnCompleted(() =>
{
// If the 'captured' localCount has not changed after awaiting the Interval,
// it indicates that no new 'bones' have been thrown during that interval.
if (capturedCount.Equals(_wdtCount))
{
onRanToCompletion();
}
});
}
}
Another nice perk is that it doesn't rely on platform timers and works just as well in iOS/Android as it does in WinForms/WPF.
For purposes of demonstration, this can be exercised in a quick console demo where the MockUpdateView() action is sent to the WDT 10 times at 500 ms intervals. It will only execute one time, 500 ms after the last restart is received.
static void Main(string[] args)
{
Console.Title = "Test WDT";
var wdt = new WatchDogTimer { Interval = TimeSpan.FromMilliseconds(500) };
Console.WriteLine(DateTime.Now.ToLongTimeString());
// "Update view 500 ms after the last restart."
for (int i = 0; i < 10; i++)
{
wdt.Restart(onRanToCompletion: ()=>MockUpdateView());
Thread.Sleep(TimeSpan.FromMilliseconds(500));
}
Console.ReadKey();
}
static void MockUpdateView()
{
Console.WriteLine($"Update now! WDT expired {DateTime.Now.ToLongTimeString()}");
}
}
So, with 500 ms times 10 restarts this verifies one event at 5 seconds from the start.
You can combine a state variable and a delay to avoid messing with timers or task cancelation. This is far simpler IMO.
Add this state variable to your class/form:
private DateTime _nextRefresh = DateTime.MaxValue;
And here's how you refresh:
private async void Update()
{
await RefreshInOneSecond();
}
private async Task RefreshInOneSecond()
{
_nextRefresh = DateTime.Now.AddSeconds(1);
await Task.Delay(1000);
if (_nextRefresh <= DateTime.Now)
{
_nextRefresh = DateTime.MaxValue;
Refresh();
}
}
If you call RefreshInOneSecond repeatedly, it pushes out the _nextRefresh timestamp until later, so any refreshes already in flight will do nothing.
Demo on DotNetFiddle
One approach is to create a timer and reset this whenever the user does something. For example using System.Timers.Timer
timer = new Timer(1000);
timer.SynchronizingObject = myControl; // Needs a winforms object for synchronization
timer.Elapsed += OnElapsed;
timer.Start(); // Don't forget to stop the timer whenever you are done
...
private void OnUserUpdate(){
timer.Interval = 1000; // Setting the interval will reset the timer
}
There are multiple timers to chose from, I believe the same pattern is possible with the other timers. DispatchTimer might be most suitable if you use WPF.
Note that both System.Timers.Timer and Task.Delay uses System.Threading.Timer in the background. It is possible to use this directly, just call the .Change method to reset it. But be aware that this raises the event on a taskpool thread, so you need to provide your own synchronization.
I implemented the same scenario in a JavaScript application using Timer. I believe it's the same in the .NET world. Anyway handling this use-case when the user calls a method repeatedly with Task.Delay() will put more pressure on GC & thread pool
var timer = new Timer()
{
Enabled = true,
Interval = TimeSpan.FromSeconds(5).TotalMilliseconds,
};
timer.Elapsed += (sender, eventArgs) =>
{
timer.Stop();
// do stuff
}
void OnKeyUp()
{
timer.Stop();
timer.Start();
}

Refreshing screen on a bad way led to application performance issues

This is how my system works:
2 computers are using different application but same database (Table: Orders).
1st computer is making orders and writing them to database,
2nd computer need to display new orders every 5 seconds.
I will shortly describe (to avoid long text) what I need to achieve and how I did it right now :
2nd computers application need "to go to database" every 5 seconds to check for new orders and show them on screen
And when there is a lot orders and when I'm doing this pretty often my application is crashing down.
This is how my app works right now:
public MainWindow()
{
try
{
InitializeComponent();
this.WindowStartupLocation = WindowStartupLocation.CenterScreen;
this.WindowState = WindowState.Maximized;
//When app runs for first time get all orders
var ordersList = OrdersController.GetOrders();
collectionViewSource.Source = ordersList;
collectionViewSource.GroupDescriptions.Add(new PropertyGroupDescription("NumberOfOrder"));
DataContext = collectionViewSource;
//Here I'm refreshing screen every 5 seconds
DispatcherTimer timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromSeconds(5));
timer.Tick += timer_Tick;
timer.Start();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
void timer_Tick(object sender, EventArgs e)
{
//Every 5 seconds get all orders from database
var ordersList = OrdersController.GetOrders();
collectionViewSource.Source = null;
collectionViewSource.Source = ordersList;
DataContext = collectionViewSource;
}
GetOrders method:
public static List<Orders> GetOrders()
{
DataServices.DB.Refresh(System.Data.Linq.RefreshMode.OverwriteCurrentValues, DataServices.DB.Orders);
var results = DataServices.DB.proc_Orders_GetAll().ToList();
List<Orders> localOrders = new List<Orders>();
foreach (var item in results)
{
Orders local = new Orders();
local.Sender = item.Sender;
local.Quantity = Convert.ToDecimal(item.Quantity);
local.ArticleTitle = item.ArticleTitle;
local.DateOfOrder = Convert.ToDateTime(item.DateOfOrder);
lokalnen.Add(local);
}
return localOrders;
}
But this code above is not so good, it's causing crushing of my application if there's a lot orders..
Probably because UI is rendering all the time again and again
Moving this "refresh job" to another task might solve the issue, so I tried something like this:
First of all create new method RefreshScreen which might look like this:
private void RefreshScreen()
{
var ordersList = OrdersController.GetOrders();
collectionViewSource.Source = null;
collectionViewSource.Source = ordersList;
DataContext = collectionViewSource;
}
And I moved this RefreshScreen() to a new task like this:
void timer_Tick(object sender, EventArgs e)
{
//Instead old code where I set source of my list and of my datagrid directlly in timer tick now I moved it to new method and calling that method from a new task
Task.Factory.StartNew(() => RefreshScreen())
.ContinueWith(task =>
{
}, System.Threading.CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
}
But this is again making my app crushs after a while...
I really don't know how to solve this, maybe something with Observablle collections or whatever,
I don't know...
Any kind of help would be awesome and great to prevent my app from crushing!
Thanks !
Edit : after running app from my visual studio I get this after few seconds (with solution that is including task in timer tick ):
You should query the database on a background thread but you can't access DataContext property or any other property of a UI element from a background thread. That's why you get the "The calling thread cannot access.." exception.
You could try to start a Task that calls the GetOrders() method on a background thread:
void timer_Tick(object sender, EventArgs e)
{
Task.Factory.StartNew(() =>
{
return OrdersController.GetOrders()
}).ContinueWith(task =>
{
collectionViewSource.Source = task.Result;
DataContext = collectionViewSource;
}, System.Threading.CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
}
But if it is the code that runs in the ContinueWith delegate once the query has completed that is slow, you have no other option than to fetch fewer records.
You should also make sure that you haven't disabled the UI virtualization. You might also want to try without grouping the results. This may be slow as well.
Try implementing SignalR. Works pretty well for that kind of operations

Trigger an Event after x seconds, but also cancel it before it executes

I'm developing an Web API (which works quite well). What's missing?
Here is sample code of Get Action:
public IEnumerable<xxxx> Get()
{
IEnumerable<xxxx> yyyy = new List<xxxx>();
//get yyyy from database
timer = new Timer();
timer.AutoReset = true;
timer.Enabled = true;
timer.Interval = 5000; //miliseconds
timer.Elapsed += timer_Elapsed;
timer.Start();
return yyyy;
}
void timer_Elapsed(object sender, ElapsedEventArgs e)
{
//code to be executed when timer elapses...
}
So once a request is received, timer will be initialized and will fire Elapsed event at interval of 5 seconds. On next subsequent request this continues....
The expected behavior is such that:
Initialize Request -1
Initialize Timer -1
If another request from same client is received within 5 seconds, timer must not fire elapsed event.
If no request is received from same client within 5 seconds, timer should elapse and fire the event.
Also the timer has nothing to do with client(s).
Here is further business scenario related to this....
I'm developing a Web API that will be consumed by an electronic device when switched on. The device will keep sending it's ON status as long as the power is available. As soon as, user turns off the switch, the request to the server stops.
These status are updated into database whether device is ON or OFF. Now the trickier part was to identify when device turns off (complicated because the server does not know anything if the device stops sending any request). So for each devices there is a separate timer.
First of all, thank you #Patrick Hofman to guide me and think out of box...
I implemented a class having static property inside it.
public class DeviceContainer
{
public static List<DevTimer> timers=new List<DevTimer>();
}
public class DevTimer:Timer
{
public string Identifier {get; set;}
public bool IsInUse{get; set;}
}
and then in above code (in question), I made following changes:
public IEnumerable<xxxx> Get(string Id)
{
//Check if timer exists in
if(!DeviceContainer.timers.Any(s=>s.Identifier.Equals(Id)))
{
//Create new object of timer, assign identifier =Id,
//set interval and initialize it. add it to collection as
var timer = new DevTimer();
timer.AutoReset = true;
timer.Enabled = true;
timer.Interval = 5000; //miliseconds
timer.Elapsed += timer_Elapsed;
timer.IsInUse=true;
timer.Identifier=Id;
DeviceContainer.timers.Add(timer);
timer.Start();
}
else
{
//Code to stop the existing timer and start it again.
var _timer=DeviceContainer.timers.FirstOrDefault(s=>s.Identifier.Equals(Id))
as DevTimer;
_timer.Stop();
_timer.Start();
}
}
void timer_Elapsed(object sender, ElapsedEventArgs e)
{
//code that will turn off the device in DB
}
I'm not posting the entire code as that's not the purpose here.
I would use Microsoft's Reactive Framework for this.
Here's the code:
IEnumerable<xxxx> yyyy = new List<xxxx>();
Subject<Unit> clientRequestArrived = new Subject<Unit>();
IDisposable subscription =
clientRequestArrived
.Select(_ => Observable.Interval(TimeSpan.FromSeconds(5.0)))
.Switch()
.Subscribe(_ =>
{
//code to be executed when timer elapses...
//directly access `yyyy` here
});
All you need to do is call clientRequestArrived.OnNext(Unit.Default); every time that a user request comes in and that will be enough for this code to reset the timer.
If you want to stop the timer entirely, just call subscription.Dispose().

Delaying execution - C# WPF

I have a Grid control and clicking on each row does some background job to load the data. Each background job is performed on a thread pool thread. When user clicks on the items quickly, lot of requests to load data will be queued. I want to minimize this by providing a delay after clicking each row. There will be some delay before firing the request to load the data.
I am thinking about using DispatcherTimer class. Something like,
readonly DispatcherTimer dt = new DispatcherTimer();
private void Clicked(object sender, RoutedEventArgs e)
{
dt.Interval = TimeSpan.FromSeconds(2);
dt.Stop();
dt.Start();
}
private void DtOnTick(object sender, EventArgs args)
{
// Fire a thread and do data loading
}
Is this the correct way to approach the problem?
Any suggestions would be appreciated!
How about disabling the control until the job is finished? Or disabling once the queue of jobs to do reaches a certain size? This would be a simple solution to prevent users from "clicking too much". And this way the delay would scale with the efficiency of your solution/speed of the computer.
The way you're trying to do it would just delay the problem itself for 2 seconds. All the clicks would just be handled two seconds later.
You might try to use a worker thread. Lets say you use a queue which takes information about each item that was clicked at the time it was clicked. An existing thread, created when the class is created, is notified when new items are added to the queue. The thread takes the first item, processes it, updates the UI. If there are more items, it takes the next one, processes it, etc. When there are no more items, the thread goes to sleep until new items are available (ManualResetEvent will help here).
The pattern would be:
void ItemClicked(...)
{
lock (WorkQueue)
{
QueueNewClickItem(...);
m_workToDo.Set();
}
}
void WorkerThread(...)
{
bool threadShouldEnd = false;
while (!threadShouldEnd)
{
if (WaitHandle.WaitAny(m_workToDo, m_endThread) == 0)
{
lock (WorkQueue)
{
CopyAllPendingWorkItemsToListInThread();
ClearWorkQueue();
m_workToDo.Reset();
}
while (!AllLocalItemsProcessed)
{
ProcessNextWorkItem();
}
}
else
{
threadShouldEnd = true;
}
}
}
What you actually want to do is something like this:
private DateTime? _NextAllowedClick;
private void Clicked(object sender, RoutedEventArgs e)
{
if (_NextAllowedClick != null && DateTime.Now < _NextAllowedClick)
{
return;
}
_NextAllowedClick = DateTime.Now + new TimeSpan(0, 0, 0, 2);
...
}

How do I add a delay after a count down timer

I am using a DispatcherTimer to perform a count down before triggering the release on a camera. The UpdateCountdown method is used to change the image displayed to the user before the camera fires. Rather than having the TakePicture method execute immediately, I would like have a slight delay after the counter reaches zero and the last image is displayed.
The code shown below results in the pause occurring at the _countdown = 1 point. While the final image displays and TakePicture() fires almost simultaneously (I think TakePicture happens first).
_countdownTimer = new DispatcherTimer();
_countdownTimer.Interval = TimeSpan.FromSeconds(1);
_countdownTimer.Tick += new EventHandler(delegate(object s, EventArgs a)
{ UpdateCountdown(); } );
_countdownTimer.Tick += new EventHandler(delegate(object s, EventArgs a)
{if (_countdown == _countdownMax)
{
System.Threading.Thread.Sleep(2000); // Slight delay before taking picture
Camera.TakePicture();
} });
}
public void StartCountdown()
{
if (doCount)
{
doCount = false;
UpdateCountdown();
_countdownTimer.Start();
}
}
private void UpdateCountdown()
{
_countdown--;
CountdownImage = _countDownImages[_countdown]; // Data bound to Image Control
if (_countdown == 0)
{
_countdown = _countdownMax;
_countdownTimer.Stop();
doCount = true;
}
What am I not taking into account with my timing?
The UI does not update immediately when you change control properties - it only updates when the thread becomes idle (that is, after all your event handlers finish executing).
Thread.Sleep blocks the thread, the event handlers don't finish executing and UI isn't redrawn.
You have to either use another timer (start a new timer on the last tick of the existing timer and call TakePicture on teh new timer's tick) or, even better, use the last tick of the existing timer - update UI when (_countdown <= _countdownMax), take picture when (_countdown == _countdownMax + 1).
Why not just make your display always show 1 less than the number of seconds remaining. That way when you get to zero, (obviously with a Math.Max(0, _countdown) to prevent showing -1) it will seem like the time has run out even though there's one more second to go.
Edit: What I meant to imply but did not state - was that you could then just have one Tick handler and not use Sleep at all which will just wind up blocking the UI anyway which will probably block your UI from updating.
I don't think that events guarantee that event handlers are triggered in the order that they are registered. Try
_countdownTimer.Tick += new EventHandler(delegate(object s, EventArgs a)
{
UpdateCountdown();
if (_countdown == _countdownMax)
{
System.Threading.Thread.Sleep(2000); // Slight delay before taking picture
Camera.TakePicture();
}
});
}

Categories

Resources