Running multiple timers simultaneously in C# using System.Threading - c#

I have service that I am using to pull up reports from a network location. The requirement is that ever 5 minutes I need to pull up report 1, every 10 minutes report 2 & every 3 hours report 3,4,5. So, I created a timer inside a method on a separate worker class where I am using 5 instances of System.Threading.Timer. The reports are getting pulled but not in proper order. How can I run the timers in such a way that reports can get pulled in a specific order or use 1 timer to schedule pulling of reports in specific manner?
Here's what I have done so far:
private async void pullReports()
{
try
{
await _logger.Info($"Start Report Monitoring");
Timer timer = new Timer
(
async e => await Service.ReportMonitoring(reportNum: 1),
null,
TimeSpan.Zero,
TimeSpan.FromMinutes(5)
);
Timer timer2 = new Timer
(
async e => await Service.ReportMonitoring(reportNum: 2),
null,
TimeSpan.Zero,
TimeSpan.FromMinutes(10)
);
Timer timer3 = new Timer
(
async e => await Service.ReportMonitoring(reportNum: 3),
null,
TimeSpan.Zero,
TimeSpan.FromHours(3)
);
Timer timer4 = new Timer
(
async e => await Service.ReportMonitoring(reportNum: 4),
null,
TimeSpan.Zero,
TimeSpan.FromHours(3)
);
Timer timer5 = new Timer
(
async e => await Service.ReportMonitoring(reportNum: 5),
null,
TimeSpan.Zero,
TimeSpan.FromHours(3)
);
}
catch (Exception ex)
{
await _logger.Error($"Start Report Monitoring exception: {ex}");
}
}
Any kind of code improvements are appreciated. I think because my method Service.ReportMonitoring() is asynchronous that is the reason that is disturbing the order of collection. But I'm not sure about that.
The code for ReportNum is as follows:
private static Task<Stream> ReportMonitoring(int repnum)
{
string _urlDataFormat = "https://{0}/cgi-bin/CGILink?cmd=vtranssetz&period=2&reptnum={1}";
string dataUrl = string.Format(_urlDataFormat, <serverIP>, repnum);
return HttpService.ExecuteGetStreamAsync(dataUrl);
}

If order of the reports is important (so the ones with higher numbers should always be after those with lower numbers) you should do something like that:
var counter = 0L;
Timer timer = new Timer
(
async _ =>
{
var local = Interlocked.Read(ref counter);
Interlocked.Increment(ref counter);
await ReportMonitoring(1);
if (local % 2 == 0)
{
await ReportMonitoring(2);
}
if (counter % (3 * 12) == 0)
{
await ReportMonitoring(3);
await ReportMonitoring(4);
await ReportMonitoring(5);
}
},
null,
TimeSpan.Zero,
TimeSpan.FromMinutes(5)
);
Also note that:
As long as you are using a Timer, you must keep a reference to it. As with any managed object, a Timer is subject to garbage collection when there are no references to it. The fact that a Timer is still active does not prevent it from being collected.

Out of curiosity I have also tried the same kind of logic with System.Timers. I created the timer instance in the class so that it won't get disposed when the method closes. In the method I have assigned required values to the timer properties. Because System.Timers have an ability to run events at intervals I have used #Guru's logic from above into the event to invoke my ReportMonitoring Method. I hope it comes handy to someone in future.
using System;
using System.Threading;
using System.Timers;
using _logger = DemoProject.Common.Service;
namespace ReportMonitoringService
{
public class Worker
{
private System.Timers.Timer timer = new System.Timers.Timer();
private double _intervalTime = 300000; // 5mins
private bool _timerInUse = false;
private long _counter = 0L;
private async void StartMonitoringTimer()
{
try
{
timer.Elapsed += new ElapsedEventHandler(this.ReportDownload);
timer.Interval = _intervalTime; //5 mins in milliseconds
timer.Enabled = true;
}
catch (Exception ex)
{
await _logger.Error($"Monitoring failed exception: {ex}");
}
}
private async void ReportDownload(object source, ElapsedEventArgs e)
{
var local = Interlocked.Read(ref _counter);
Interlocked.Increment(ref _counter);
if (_timerInUse == false)
{
_timerInUse = true;
try
{
int _numReports;
if (local % 2 == 0) { _numReports = 2; }
else if (_counter % (3 * 12) == 0) { _numReports = 5; }
else { _numReports = 1; }
await ReportMonitoring(_numReports);
}
catch (Exception ex)
{
await _logger.Error($"Timer function threw exception downloading from Server");
await AppendLogtoFile("error: {ex.Message} stack trace: {ex.StackTrace}", LogType.Error);
}
_timerInUse = false;
}
}
}
}

Related

How do i stop pause a task in c# .net

I have tried but it is not working i want to stop and pause this task:: code below
private void checkingTimer()
{
System.Windows.Threading.DispatcherTimer dispatcherTimer = new System.Windows.Threading.DispatcherTimer();
dispatcherTimer.Tick += new EventHandler(startPollingAwaitingURLs);
dispatcherTimer.Interval = new TimeSpan(0, 0, 0, 0, 1000);
dispatcherTimer.Start();
}
private static object _lock_CrawlingSync = new object();
private static bool blBeingProcessed = false;
private static List<Task> lstCrawlingTasks = new List<Task>();
private static List<string> lstCurrentlyCrawlingUrls = new List<string>();
private void btnTest_Click(object sender, RoutedEventArgs e)
{
using (WebCrawlerEntities1 db = new WebCrawlerEntities1())
{
db.tblMainUrls.RemoveRange(db.tblMainUrls);
db.SaveChanges();
}
}
private void clearDBandStart(object sender, RoutedEventArgs e)
{
dtStartDate = DateTime.Now;
crawlPage(urlTextbox.Text.normalizeUrl(), 0, urlTextbox.Text.normalizeUrl(), DateTime.Now);
checkingTimer();
}
private void startPollingAwaitingURLs(object sender, EventArgs e)
{
lock (UserLogs)
{
string srPerMinCrawlingspeed = (irCrawledUrlCount.ToDouble() / (DateTime.Now - dtStartDate).TotalMinutes).ToString("N2");
string srPerMinDiscoveredLinkSpeed = (irDiscoveredUrlCount.ToDouble() / (DateTime.Now - dtStartDate).TotalMinutes).ToString("N2");
string srPassedTime = (DateTime.Now - dtStartDate).TotalMinutes.ToString("N2");
UserLogs.Insert(0, $"{DateTime.Now} polling awaiting urls \t processing: {blBeingProcessed} \t number of crawling tasks: {lstCrawlingTasks.Count}");
UserLogs.Insert(0, $"Total Time: {srPassedTime} Minutes \t Total Crawled Links Count: {irCrawledUrlCount.ToString("N0")} \t Crawling Speed Per Minute: {srPerMinCrawlingspeed} \t Total Discovered Links : {irDiscoveredUrlCount.ToString("N0")} \t Discovered Url Speed: {srPerMinDiscoveredLinkSpeed} ");
}
logMesssage($"polling awaiting urls \t processing: {blBeingProcessed} \t number of crawling tasks: {lstCrawlingTasks.Count}");
if (blBeingProcessed)
return;
lock (_lock_CrawlingSync)
{
blBeingProcessed = true;
lstCrawlingTasks = lstCrawlingTasks.Where(pr => pr.Status != TaskStatus.RanToCompletion && pr.Status != TaskStatus.Faulted).ToList();
int irTasksCountToStart = _irNumberOfTotalConcurrentCrawling - lstCrawlingTasks.Count;
if (irTasksCountToStart > 0)
using (WebCrawlerEntities1 db = new WebCrawlerEntities1())
{
var vrReturnedList = db.tblMainUrls.Where(x => x.isCrawled == false && x.CrawlTryCounter < _irMaximumTryCount)
.OrderBy(pr => pr.DiscoverDate)
.Select(x => new
{
x.Url,
x.LinkDepthLevel
}).Take(irTasksCountToStart * 2).ToList();
logMesssage(string.Join(" , ", vrReturnedList.Select(pr => pr.Url)));
foreach (var vrPerReturned in vrReturnedList)
{
var vrUrlToCrawl = vrPerReturned.Url;
int irDepth = vrPerReturned.LinkDepthLevel;
lock (lstCurrentlyCrawlingUrls)
{
if (lstCurrentlyCrawlingUrls.Contains(vrUrlToCrawl))
{
logMesssage($"bypass url since already crawling: \t {vrUrlToCrawl}");
continue;
}
lstCurrentlyCrawlingUrls.Add(vrUrlToCrawl);
}
logMesssage($"starting crawling url: \t {vrUrlToCrawl}");
lock (UserLogs)
{
UserLogs.Insert(0, $"{DateTime.Now} starting crawling url: \t {vrUrlToCrawl}");
}
var vrStartedTask = Task.Factory.StartNew(() => { crawlPage(vrUrlToCrawl, irDepth, null, DateTime.MinValue); }).ContinueWith((pr) =>
{
lock (lstCurrentlyCrawlingUrls)
{
lstCurrentlyCrawlingUrls.Remove(vrUrlToCrawl);
logMesssage($"removing url from list since task completed: \t {vrUrlToCrawl}");
}
});
lstCrawlingTasks.Add(vrStartedTask);
if (lstCrawlingTasks.Count > _irNumberOfTotalConcurrentCrawling)
break;
}
}
blBeingProcessed = false;
}
}
so is there a way to stop my task up ahead and pause should i mre.set() or .Set()
my application is a web crawler that get links from any website.
so when pressing on a button web crawling task pause or stop and restart ... any methods to do or changes
Try :
Thread.Sleep() to pause your task.
Thread.Interrupt() to stop your sleeping task.
Technically you can't stop a Task. Task isn't running anything but only waiting for a completion of something it was bound for. You can stop the code, running in the Thread. For example, with ManualResetEventSlim.
private readonly ManualResetEventSlim _mre = new ManualResetEventSlim(true); // not paused initially
public void Pause()
{
_mre.Reset();
}
public void Resume()
{
_mre.Set();
}
In the method where you want to pause the execution when requested, just add.
_mre.Wait();
Few tips
Use Task.Run instead of Task.Factory.StartNew and learn the difference.
Find something about Producer/Consumer programming pattern and related thread-safe collections.
Say hello to Asynchronous programming.

Wait for all threads to release semaphore?

How do I close down and wait for a semaphore to be fully released?
private SemaphoreSlim _processSemaphore = new SemaphoreSlim(10);
public async Task<Modification> Process(IList<Command> commands)
{
Assert.IsFalse(_shuttingDown, "Server is in shutdown phase");
await _processSemaphore.WaitAsync();
try
{
// threads that have reached this far must be allowed to complete
return _database.Process(commands);
}
finally
{
_processSemaphore.Release();
}
}
public async Task StopAsync()
{
_shuttingDown = true;
// how wait for threads to complete without cancelling?
await ?
}
private SemaphoreSlim _processSemaphore = new SemaphoreSlim(10);
private int _concurrency;
private TaskCompletionSource<int> _source;
private ManualResetEvent _awaitor;
public void Start()
{
//solution 1
_concurrency = 0;
_source = new TaskCompletionSource<int>();
_shuttingDown = false;
//solution 2
_awaitor = new ManualResetEvent(false);
//your code
}
public async Task<Modification> Process(IList<Command> commands)
{
Interlocked.Increment(ref _concurrency);
Assert.IsFalse(_shuttingDown, "Server is in shutdown phase");
await _processSemaphore.WaitAsync();
try
{
// threads that have reached this far must be allowed to complete
return _database.Process(commands);
}
finally
{
_processSemaphore.Release();
//check and release
int concurrency = Interlocked.Decrement(ref _concurrency);
if (_shuttingDown && concurrency == 0)
{
//solution 1
_source.TrySetResult(0);
//solution 2
_awaitor.Set();
}
}
}
public async Task StopAsync()
{
_shuttingDown = true;
// how wait for threads to complete without cancelling?
if (Interlocked.CompareExchange(ref _concurrency, 0, 0) != 0)
{
await _source.Task;//solution 1
_awaitor.WaitOne();//solution 2
}
}
Might not be exactly what you need, but I had a similar case and I solved it with the CountdownEvent class
private CountdownEvent _countdownEvent = new CountdownEvent(1);
process_method() {
//if the count is zero means that we already called finalize
if (_countdownEvent.IsSet)
return;
try
{
//this can throw and exception if we try to add when the countdown has already reached 0.
//this exception happens when one process_method B has passed the _counddownEvent.IsSet check and context switched to
//to another process_method A that was the last one (after finalize waits for 0) and sets the countdown to 0. Which
//triggers finalization and should not allow new process_method, so process_methodB not continuing is good (finalization is
//in progress).
_countdownEvent.AddCount(1);
} catch
{
return;
}
try
{
//your process
}
finally
{
_countdownEvent.Signal();
}
}
And then when you are ready to wait for the count to be zero:
finalize() {
_countdownEvent.Signal();
_countdownEvent.Wait(_finalizationSafetyTimeout, cancellationToken)
}

How to handle Exceptions in System.Threading.Timer

I'm trying to handle exception but it doesn't work!
whats the problem?
lets say in Main i run SetUpTimer with any parameters.
private void SetUpTimer(TimeSpan alertTime, string name)
{
DateTime current = DateTime.Now;
TimeSpan timeToGo = alertTime - current.TimeOfDay;
try
{
Timer timer = new Timer(x => RunReportTask(name),null, timeToGo, Timeout.InfiniteTimeSpan));
catch(Exception e)
{
Console.WriteLine("Log Exception....")
}
}
private void RunReportTask(string name)
{
Console.WriteLine("\r\n\r\nSTART Task \t\tReport: " + name);
//DELAY 25 sec
Task.Delay(25000);
if (name.Equals("Report A") || name.Equals("Report D") || name.Equals("Report F"))
{
throw new Exception("Task failed!!!");
}
else
{
Console.WriteLine("Task: \t\t" + name + "\tDONE.");
}
}
so now if i execute it will throw an exception with Report name A,D,F. but it won't catch it, but why? and what can i do about it?
The Timer calls the TimerCallback on a separate ThreadPool thread.
Exceptions thrown in a TimerCallback method are not propagated to the code/thread where the timer was created (they just 'disapppear').
If you want to handle them outside of your callback method: you should catch them in the callback method and use some mechanism to rethrow or handle them in the original thread.
Personally I like the IProgress for this.
There are a number of syntax errors in your original code, but based on that the following example should work:
private Timer timer;
public void SetUpTimer(DateTime alertTime, string name)
{
var progress = new Progress<Exception>((e) =>
{
// handle exception form timercallback here, or just rethrow it...
throw e;
});
DateTime current = DateTime.Now;
TimeSpan timeToGo = (alertTime - current);
timer = new Timer(x => RunReportTask(name, progress),
null, timeToGo, Timeout.InfiniteTimeSpan);
}
private void RunReportTask(string name, IProgress<Exception> progress)
{
try
{
Console.WriteLine("\r\n\r\nSTART Task \t\tReport: " + name);
//DELAY 25 sec
Task.Delay(25000);
if(name.Equals("Report A") || name.Equals("Report D") || name.Equals("Report F"))
{
throw new Exception("Task failed!!!");
}
else
{
Console.WriteLine("Task: \t\t" + name + "\tDONE.");
}
}
catch(Exception e)
{
progress.Report(e);
}
}
Warning: Apart from the syntax, there is an important issue with the code in your example. You have to keep a reference to your Timer instance for as long as you want to have it active. Whenever a Timer goes out of scope, it becomes a candidate for garbage collection.
In your example the Timeris a local variable which goes out of scope as soon as the method finishes. If it is garbage collected before alertTime, the TimerCallback will never be called. For that reason i have promoted your Timerto a private field of your class.
Furthermore, when you no longer need the Timer, you should call Dispose() on the Timer to release its resources (e.g. in a Dispose methode for your class).
Try this one (because you did not close brackets in try block):
private void SetUpTimer(TimeSpan alertTime, string name)
{
DateTime current = DateTime.Now;
TimeSpan timeToGo = alertTime - current.TimeOfDay;
try
{
Timer timer = new Timer(x => RunReportTask(name),null, timeToGo, Timeout.InfiniteTimeSpan));
}
catch(Exception e)
{
Console.WriteLine("Log Exception....")
}
}

TaskFactory, Starting a new Task when one ends

I have found many methods of using the TaskFactory but I could not find anything about starting more tasks and watching when one ends and starting another one.
I always want to have 10 tasks working.
I want something like this
int nTotalTasks=10;
int nCurrentTask=0;
Task<bool>[] tasks=new Task<bool>[nThreadsNum];
for (int i=0; i<1000; i++)
{
string param1="test";
string param2="test";
if (nCurrentTask<10) // if there are less than 10 tasks then start another one
tasks[nCurrentThread++] = Task.Factory.StartNew<bool>(() =>
{
MyClass cls = new MyClass();
bool bRet = cls.Method1(param1, param2, i); // takes up to 2 minutes to finish
return bRet;
});
// How can I stop the for loop until a new task is finished and start a new one?
}
Check out the Task.WaitAny method:
Waits for any of the provided Task objects to complete execution.
Example from the documentation:
var t1 = Task.Factory.StartNew(() => DoOperation1());
var t2 = Task.Factory.StartNew(() => DoOperation2());
Task.WaitAny(t1, t2)
I would use a combination of Microsoft's Reactive Framework (NuGet "Rx-Main") and TPL for this. It becomes very simple.
Here's the code:
int nTotalTasks=10;
string param1="test";
string param2="test";
IDisposable subscription =
Observable
.Range(0, 1000)
.Select(i => Observable.FromAsync(() => Task.Factory.StartNew<bool>(() =>
{
MyClass cls = new MyClass();
bool bRet = cls.Method1(param1, param2, i); // takes up to 2 minutes to finish
return bRet;
})))
.Merge(nTotalTasks)
.ToArray()
.Subscribe((bool[] results) =>
{
/* Do something with the results. */
});
The key part here is the .Merge(nTotalTasks) which limits the number of concurrent tasks.
If you need to stop the processing part way thru just call subscription.Dispose() and everything gets cleaned up for you.
If you want to process each result as they are produced you can change the code from the .Merge(...) like this:
.Merge(nTotalTasks)
.Subscribe((bool result) =>
{
/* Do something with each result. */
});
This should be all you need, not complete, but all you need to do is wait on the first to complete and then run the second.
Task.WaitAny(task to wait on);
Task.Factory.StartNew()
Have you seen the BlockingCollection class? It allows you to have multiple threads running in parallel and you can wait from results from one task to execute another. See more information here.
The answer depends on whether the tasks to be scheduled are CPU or I/O bound.
For CPU-intensive work I would use Parallel.For() API setting the number of thread/tasks through MaxDegreeOfParallelism property of ParallelOptions
For I/O bound work the number of concurrently executing tasks can be significantly larger than the number of available CPUs, so the strategy is to rely on async methods as much as possible, which reduces the total number of threads waiting for completion.
How can I stop the for loop until a new task is finished and start a
new one?
The loop can be throttled by using await:
static void Main(string[] args)
{
var task = DoWorkAsync();
task.Wait();
// handle results
// task.Result;
Console.WriteLine("Done.");
}
async static Task<bool> DoWorkAsync()
{
const int NUMBER_OF_SLOTS = 10;
string param1="test";
string param2="test";
var results = new bool[NUMBER_OF_SLOTS];
AsyncWorkScheduler ws = new AsyncWorkScheduler(NUMBER_OF_SLOTS);
for (int i = 0; i < 1000; ++i)
{
await ws.ScheduleAsync((slotNumber) => DoWorkAsync(i, slotNumber, param1, param2, results));
}
ws.Complete();
await ws.Completion;
}
async static Task DoWorkAsync(int index, int slotNumber, string param1, string param2, bool[] results)
{
results[slotNumber] = results[slotNumber} && await Task.Factory.StartNew<bool>(() =>
{
MyClass cls = new MyClass();
bool bRet = cls.Method1(param1, param2, i); // takes up to 2 minutes to finish
return bRet;
}));
}
A helper class AsyncWorkScheduler uses TPL.DataFlow components as well as Task.WhenAll():
class AsyncWorkScheduler
{
public AsyncWorkScheduler(int numberOfSlots)
{
m_slots = new Task[numberOfSlots];
m_availableSlots = new BufferBlock<int>();
m_errors = new List<Exception>();
m_tcs = new TaskCompletionSource<bool>();
m_completionPending = 0;
// Initial state: all slots are available
for(int i = 0; i < m_slots.Length; ++i)
{
m_slots[i] = Task.FromResult(false);
m_availableSlots.Post(i);
}
}
public async Task ScheduleAsync(Func<int, Task> action)
{
if (Volatile.Read(ref m_completionPending) != 0)
{
throw new InvalidOperationException("Unable to schedule new items.");
}
// Acquire a slot
int slotNumber = await m_availableSlots.ReceiveAsync().ConfigureAwait(false);
// Schedule a new task for a given slot
var task = action(slotNumber);
// Store a continuation on the task to handle completion events
m_slots[slotNumber] = task.ContinueWith(t => HandleCompletedTask(t, slotNumber), TaskContinuationOptions.ExecuteSynchronously);
}
public async void Complete()
{
if (Interlocked.CompareExchange(ref m_completionPending, 1, 0) != 0)
{
return;
}
// Signal the queue's completion
m_availableSlots.Complete();
await Task.WhenAll(m_slots).ConfigureAwait(false);
// Set completion
if (m_errors.Count != 0)
{
m_tcs.TrySetException(m_errors);
}
else
{
m_tcs.TrySetResult(true);
}
}
public Task Completion
{
get
{
return m_tcs.Task;
}
}
void SetFailed(Exception error)
{
lock(m_errors)
{
m_errors.Add(error);
}
}
void HandleCompletedTask(Task task, int slotNumber)
{
if (task.IsFaulted || task.IsCanceled)
{
SetFailed(task.Exception);
return;
}
if (Volatile.Read(ref m_completionPending) == 1)
{
return;
}
// Release a slot
m_availableSlots.Post(slotNumber);
}
int m_completionPending;
List<Exception> m_errors;
BufferBlock<int> m_availableSlots;
TaskCompletionSource<bool> m_tcs;
Task[] m_slots;
}

cancel async task if running

I have the following method called on several occasions (e.g onkeyup of textbox) which asynchronously filters items in listbox.
private async void filterCats(string category,bool deselect)
{
List<Category> tempList = new List<Category>();
//Wait for categories
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
//HERE,CANCEL TASK IF ALREADY RUNNING
tempList= await _filterCats(category,token);
//Show results
CAT_lb_Cats.DataSource = tempList;
CAT_lb_Cats.DisplayMember = "strCategory";
CAT_lb_Cats.ValueMember = "idCategory";
}
and the following task
private async Task<List<Category>> _filterCats(string category,CancellationToken token)
{
List<Category> result = await Task.Run(() =>
{
return getCatsByStr(category);
},token);
return result;
}
and I would like to test whether the task is already runing and if so cancel it and start it with the new value. I know how to cancel task, but how can I check whether it is already running?
This is the code that I use to do this :
if (_tokenSource != null)
{
_tokenSource.Cancel();
}
_tokenSource = new CancellationTokenSource();
try
{
await loadPrestatieAsync(_bedrijfid, _projectid, _medewerkerid, _prestatieid, _startDate, _endDate, _tokenSource.Token);
}
catch (OperationCanceledException ex)
{
}
and for the procedure call it is like this (simplified of course) :
private async Task loadPrestatieAsync(int bedrijfId, int projectid, int medewerkerid, int prestatieid,
DateTime? startDate, DateTime? endDate, CancellationToken token)
{
await Task.Delay(100, token).ConfigureAwait(true);
try{
//do stuff
token.ThrowIfCancellationRequested();
}
catch (OperationCanceledException ex)
{
throw;
}
catch (Exception Ex)
{
throw;
}
}
I am doing a delay of 100 ms because the same action is triggered rather quickly and repeatedly, a small postpone of 100 ms makes it look like the GUI is more responsive actually.
It appears you are looking for a way to get an "autocomplete list" from text entered in a text box, where an ongoing async search is canceled when the text has changed since the search was started.
As was mentioned in the comments, Rx (Reactive Extensions), provides very nice patterns for this, allowing you to easily connect your UI elements to cancellable asynchronous tasks, building in retry logic, etc.
The less than 90 line program below, shows a "full UI" sample (unfortunately excluding any cats ;-). It includes some reporting on the search status.
I have created this using a number of static methods in the RxAutoComplete class, to show how to this is achieved in small documented steps, and how they can be combined, to achieve a more complex task.
namespace TryOuts
{
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Reactive.Linq;
using System.Threading;
// Simulated async search service, that can fail.
public class FakeWordSearchService
{
private static Random _rnd = new Random();
private static string[] _allWords = new[] {
"gideon", "gabby", "joan", "jessica", "bob", "bill", "sam", "johann"
};
public async Task<string[]> Search(string searchTerm, CancellationToken cancelToken)
{
await Task.Delay(_rnd.Next(600), cancelToken); // simulate async call.
if ((_rnd.Next() % 5) == 0) // every 5 times, we will cause a search failure
throw new Exception(string.Format("Search for '{0}' failed on purpose", searchTerm));
return _allWords.Where(w => w.StartsWith(searchTerm)).ToArray();
}
}
public static class RxAutoComplete
{
// Returns an observable that pushes the 'txt' TextBox text when it has changed.
static IObservable<string> TextChanged(TextBox txt)
{
return from evt in Observable.FromEventPattern<EventHandler, EventArgs>(
h => txt.TextChanged += h,
h => txt.TextChanged -= h)
select ((TextBox)evt.Sender).Text.Trim();
}
// Throttles the source.
static IObservable<string> ThrottleInput(IObservable<string> source, int minTextLength, TimeSpan throttle)
{
return source
.Where(t => t.Length >= minTextLength) // Wait until we have at least 'minTextLength' characters
.Throttle(throttle) // We don't start when the user is still typing
.DistinctUntilChanged(); // We only fire, if after throttling the text is different from before.
}
// Provides search results and performs asynchronous,
// cancellable search with automatic retries on errors
static IObservable<string[]> PerformSearch(IObservable<string> source, FakeWordSearchService searchService)
{
return from term in source // term from throttled input
from result in Observable.FromAsync(async token => await searchService.Search(term, token))
.Retry(3) // Perform up to 3 tries on failure
.TakeUntil(source) // Cancel pending request if new search request was made.
select result;
}
// Putting it all together.
public static void RunUI()
{
// Our simple search GUI.
var inputTextBox = new TextBox() { Width = 300 };
var searchResultLB = new ListBox { Top = inputTextBox.Height + 10, Width = inputTextBox.Width };
var searchStatus = new Label { Top = searchResultLB.Height + 30, Width = inputTextBox.Width };
var mainForm = new Form { Controls = { inputTextBox, searchResultLB, searchStatus }, Width = inputTextBox.Width + 20 };
// Our UI update handlers.
var syncContext = SynchronizationContext.Current;
Action<Action> onUITread = (x) => syncContext.Post(_ => x(), null);
Action<string> onSearchStarted = t => onUITread(() => searchStatus.Text = (string.Format("searching for '{0}'.", t)));
Action<string[]> onSearchResult = w => {
searchResultLB.Items.Clear();
searchResultLB.Items.AddRange(w);
searchStatus.Text += string.Format(" {0} maches found.", w.Length > 0 ? w.Length.ToString() : "No");
};
// Connecting input to search
var input = ThrottleInput(TextChanged(inputTextBox), 1, TimeSpan.FromSeconds(0.5)).Do(onSearchStarted);
var result = PerformSearch(input, new FakeWordSearchService());
// Running it
using (result.ObserveOn(syncContext).Subscribe(onSearchResult, ex => Console.WriteLine(ex)))
Application.Run(mainForm);
}
}
}

Categories