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
Related
Background
Currently working on a windows form app which I asked to create. I have ran into an issue where the UI freezes when a resource intensive process is being called. I am currently using threading from which I understand is used to prevent the UI from freezing and taking over the entire pc.
Question
Currently when I am using threading to call a method in my base class which is to open a file that is located on a remote server. This method has a delay of approximately 30 to 45 seconds. I am creating my background thread and invoking it to start. When invoked to start if fires, however when it fired it would not wait for my thread to complete basically giving me a null exception. So after some digging I found that in order to wait for the thread to complete you had to invoke the .Join(). However when the Join is invoked it froze my UI completely. So my ingenuity tried to create a work around and created a while loop that would until the thread is no longer alive and continue. However, this also froze the UI. So am I missing something? That is not mention in MSDN Doc
Code Sample
class BaseClass
{
public CWClient ClientFileContext(string clientFile, bool compress)
{
Client clientContext = null;
try
{
if (compress == true)
{
clientContext = appInstance.Clients.Open2(clientFile, superUser, passWord, OpenFlags.ofCompressed);
}
else
{
clientContext = appInstance.Clients.Open2(clientFile, superUser, passWord, OpenFlags.ofNone);
}
}
catch (Exception ex)
{
//TODO
}
return clientContext;
}
}
public partial class Form1 : Form
{
private void button1_Click(object sender, EventArgs e)
{
BaseClass wpSec = new BaseClass();
CWClient client = null;
Thread backgroundThread = new Thread(
new ThreadStart(() =>
{
client = wpSec.ClientFileContext(selectedFileFullPath, true);
}
));
backgroundThread.Start();
//backgroundThread.Join(); << Freezes the UI
var whyAreYouNotWorking = "Stop";
}
}
Work around I tried
while (backgroundThread.IsAlive == true)
{
for (int n = 0; n < 100; n++)
{
Thread.Sleep(500);
progressBar1.BeginInvoke(new Action(() => progressBar1.Value = n));
}
}
// This also freezes the UI
I would also look into the async and await pattern for this. Explained in this post: Using async await still freezes GUI
Your code should be similar to this (Baseclass doesn't change) :
public partial class Form1 : Form
{
private async void button1_Click(object sender, EventArgs e)
{
BaseClass wpSec = new BaseClass();
CWClient client = await Task.Run(() =>
{
return wpSec.ClientFileContext(selectedFileFullPath, true);
}
);
var whyAreYouNotWorking = "Stop";
}
}
This is back-of-the-envelope stuff, but hopefully that gives the basic idea of launching a task, then awaiting the result in an async method. If you don't need your BaseClass hanging around, that can be in the lambda too, leaving you only what you really want.
That link from #Chris Dunaway above is also excellent. http://blog.stephencleary.com/2013/08/taskrun-vs-backgroundworker-round-3.html
Edit: As #BradlyUffner mentions, this is also one of the few times you should use async void and should rather prefer returning Task or Task<T> in virtually all other circumstances.
I'm learning how to webscrape in WPF. I check the site every 20sec, update my ObservableCollection (myClients) according to search results and display it in Listview (myList). I have 2 Buttons, one to start search and one to stop it.
I didn't know how to implement button autoclick every X sec (which would solve all my problems, am i right?) so i had to use Task.Delay(20000). Program works, it doesn't freeze right at the start like if i had used Thread.Sleep(), but if i press the Stop button and then Start, everything freezes.
I will upload only portion of the code that seems to be the problem. Note that the whole program at the moment is mostly reverse-engineered from several different programs as i am still a beginner.
private async void Button_Click(object sender, RoutedEventArgs e) //Start button
{
string car;
string price;
string link;
wantToAbort = false;
while (!wantToAbort)
{
// ----Simulate GET request----
//-----End GET----
myList.ItemsSource = myClients;
string searchCar = txtBlock.Text + " " + txtBlock2.Text;
var articleNodes = htmlDoc.DocumentNode.SelectNodes($"//*[#id='main_content']/div[1]/div[2]/ul[1]//*[text()[contains(., '{searchCar}')]]");
if (articleNodes != null && articleNodes.Any())
{
foreach (var articleNode in articleNodes)
{
car = WebUtility.HtmlDecode(articleNode.InnerText);
price = WebUtility.HtmlDecode(articleNode.ParentNode.ParentNode.SelectSingleNode("span").InnerText);
link = WebUtility.HtmlDecode(articleNode.ParentNode.ParentNode.Attributes["href"].Value);
var tempUser = new User(car, price, link);
if (!myClients.Any(x=>x.Link == tempUser.Link))
{
myClients.Insert(0, tempUser); //Inserts new item if Links are different
txtBlock3.Text = "Searching...";
}
}
await Task.Delay(20000); //This seems to be an issue
}
}
}
private void Button_Click_1(object sender, RoutedEventArgs e) //Stop button
{
wantToAbort = true;
txtBlock3.Text = "Ready to search again!";
}
Running a while loop on the UI thread may freeze the application as the UI thread cannot both process UI events and execute a loop or doing anything else simultaneously.
If you want to do something every x seconds you could use a timer as suggested by EJoshuaS. There is a DispatcherTimer class in WPF that fires a Tick event on the UI thread at an interval specified by the Interval property: https://msdn.microsoft.com/en-us/library/system.windows.threading.dispatchertimer%28v=vs.110%29.aspx
You don't want to perform the GET request to the web server on the UI thread though so you should probably use a System.Timer.Timer: https://msdn.microsoft.com/en-us/library/system.timers.timer(v=vs.110).aspx. This is a different type of timer that runs on a background thread.
Since you can only access UI controls such as TextBlocks and ListBoxes on the thread on which they were originally created - that is the UI thread - you will have to use the dispatcher to marshall any code that access these controls back to the UI thread in your Elapsed event handler:
private static void OnTimedEvent(Object source, ElapsedEventArgs e)
{
//call the web server here....
//dispatch any access to any UI control
txtBlock3.Dispatcher.Invoke(new Action(() = > { txtBlock3.Text = "Searching..."; }));
}
The golden rule to maintain a responsive application is to execute any long-running code on a background thread but you must only access UI controls back on the UI thread. Please refer to MSDN for more information about the threading model in WPF: https://msdn.microsoft.com/en-us/library/ms741870(v=vs.110).aspx
DispatcherTimer may be a better solution in this case, like in the below example:
public partial class MainWindow : Window
{
private DispatcherTimer timer;
public MainWindow()
{
InitializeComponent();
timer = new DispatcherTimer();
timer.Interval = new TimeSpan(0, 0, 220);
timer.Tick += Timer_Tick;
timer.Start();
}
private void Timer_Tick(object sender, EventArgs e)
{
// Do something on your UI
Trace.TraceInformation("Timer expired");
}
}
Basically, this will raise an event at a given interval. Note that Windows Forms also has a timer, as does System.Threading, but you want to make sure you use DispatcherTimer rather than those. In particular, the one from System.Threading tends not to mix well with UIs because it runs its actions on the thread pool and WPF in particular is very fussy about how you update your UI from background threads.
The documentation I link to, as well as this answer, also give details on this.
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.
I've an existing WPF application, which has several sections. Every section is a UserControl, that implements an interface.
The interface specify two methods: void LoadData([...]) and bool UnloadData().
Those method are called by the UI thread, so we need to do our work in backgroundworker if it's time consuming.
No problems with LoadData since we can update the UI asynchronously. The problem is with UnloadData().
This should return if we can really leave the current view.
This is computed with the current status of data(Saved/modified/Invalid):
Saved return true,
Invalid asks if you want to stay to save some
correct data or leave without saving
Modified tell you that you can
either cancel your change(return true), either continue to
edit(return false), either save you current data(return true)
The problem is with the "Modified -> Save". This is a time consuming method, so to respect the philosophy of the application, we should run this in a background thread(with a busy indicator).
But if we just launch the thread and go to the next section, it will return "true" to the method call, and we will directly launch the next view.
In my case, loading the next view before our local data is saved can be a problem.
So:
Is there a way to wait on the background thread to finish before returning "true", WITHOUT blocking the UI?
public bool UnloadData(){
if(...){
LaunchMyTimeConsumingMethodWithBackgroundWorker();
return true;//Only when my time consuming method ends
}
//[...]
}
Important EDIT
Maybe I wasn't clear enought: I know how to use a BackgroundWorker, or TPL. My problem is that the parent class(the one which call the UnloadData()" is a class that I cannot edit(for multiple reasons: It's in another DLL that will not be reloaded, it already works with 70+ userControls, all in separate projects(dll), loaded by reflection.
This wasn't my choice, I don't find it good, but I've to deal with it now. I'm mostly looking for way to make my method wait on the return of my method. I'm not sure if it is possible. But I'm looking for a workaround, it will spare me weeks of works.
Ok now I'm excited, because I think I may have discovered something on my own...
So, what you do is this: You create a DispatcherFrame, push that frame onto the Dispatcher, and in the RunWorkerCompleted you set the Continue of the Frame to false.
This is the code so far:
public void Function()
{
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += TimeConsumingFunction;
var frame = new DispatcherFrame();
worker.RunWorkerCompleted += (sender, args) =>
{
frame.Continue = false;
};
worker.RunWorkerAsync();
Dispatcher.PushFrame(frame);
}
private void TimeConsumingFunction(object sender, DoWorkEventArgs doWorkEventArgs)
{
Console.WriteLine("Entering");
for (int i = 0; i < 3; i++)
{
Thread.Sleep(1000);
}
Console.WriteLine("Exiting");
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
Function();
Console.WriteLine("Returns");
}
You should implement a dependency property "IsBusy" of type bool, that you set to TRUE before starting the BackgoundWorker, and then to FALSE when the work is complete.
On the UI, you bind to that property whatever functionality you want disabled during the processing(like the button for loading the next view, etc.); or maybe showing a "Cancel" button.
You should not "wait" for the operation to complete, you can retrieve the result in an additional variable, that the BackgroundWorker will set:
BackgroundWorker _bw;
bool _returnValue = false;
private void button_Click(object sender, RoutedEventArgs e)
{ // if starting the processing by clicking a button
_bw = new BackgroundWorker();
IsBusy = true;
_bw.DoWork += new DoWorkEventHandler(_bw_DoWork);
_bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(_bw_RunWorkerCompleted);
_bw.RunWorkerAsync();
}
void _bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
IsBusy = false;
// retrieve the result of the operation in the _returnValue variable
}
void _bw_DoWork(object sender, DoWorkEventArgs e)
{
_returnValue = UnloadData();
}
private bool UnloadData()
{
if (...)
{
LaunchTimeConsumingMethod();
return true;
}
else
return false;
//etc ...
}
public bool IsBusy
{
get { return (bool)GetValue(IsBusyProperty); }
set { SetValue(IsBusyProperty, value); }
}
// Using a DependencyProperty as the backing store for IsBusy. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsBusyProperty =
DependencyProperty.Register( ... )
You may be able to try using the new "await" features of .NET 4.5.
The await keyword allows you to await the completion of a Task object, without blocking the UI.
Try this modification:
public async bool UnloadData()
{
if(...)
{
await Task.Factory.StartNew(() =>
{
LaunchMyTimeConsumingMethod();
});
return true;//Only when my time consuming method ends
}
//[...]
}
Treat UnloadData as a async operation and let the async/await features handle both the case when it completes synchronously and when it needs to complete asynchronously:
public async Task<bool> UnloadData(){
if(...){
// The await keyword will segment your method execution and post the continuation in the UI thread
// The Task.Factory.StartNew will run the time consuming method in the ThreadPool
await Task.Factory.StartNew(()=>LaunchMyTimeConsumingMethodWithBackgroundWorker());
// The return statement is the continuation and will run in the UI thread after the consuming method is executed
return true;
}
// If it came down this path, the execution is synchronous and is completely run in the UI thread
return false;
}
private async void button_Click(object sender, RoutedEventArgs e)
{
// Put here your logic to prevent user interaction during the operation's execution.
// Ex: this.mainPanel.IsEnabled = false;
// Or: this.modalPanel.Visibility = Visible;
// etc
try
{
bool result = await this.UnloadData();
// Do whatever with the result
}
finally
{
// Reenable the user interaction
// Ex: this.mainPanel.IsEnabled = true;
}
}
EDIT
If you can't modify the UnloadData, then just execute it on the ThreadPool, as #BTownTKD noted:
private async void button_Click(object sender, RoutedEventArgs e)
{
// Put here your logic to prevent user interaction during the operation's execution.
// Ex: this.mainPanel.IsEnabled = false;
// Or: this.modalPanel.Visibility = Visible;
// etc
try
{
// The await keyword will segment your method execution and post the continuation in the UI thread
// The Task.Factory.StartNew will run the time consuming method in the ThreadPool, whether it takes the long or the short path
bool result = await The Task.Factory.StartNew(()=>this.UnloadData());
// Do whatever with the result
}
finally
{
// Reenable the user interaction
// Ex: this.mainPanel.IsEnabled = true;
}
}
You probably should use TPL if your framework version is 4.0:
var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); // this will work only if you're running this code from UI thread, for example, by clicking a button
Task.Factory.StartNew(() => UnloadData()).ContinueWith(t => /*update ui using t.Result here*/, uiScheduler);
Hope this helps.
You have to implement a callback function (RunWorkerCompleted), this is called when the background worker finishes.
Check out an example here:
http://msdn.microsoft.com/en-us/library/cc221403(v=vs.95).aspx
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);
...
}