Blazor: Using a keyboard event together with DebounceInterval in a MudBlazor textfield - binding

I am using MudBlazor to develop a client-side Blazor-WASM app. I have a search field which is to update the bound variable in real-time (i.e. not only after losing focus, but while typing). Furthermore, I want to clear the MudTextField's text if user presses the -key (so they don't have to use the clear-button if they don't want to). Well, if I do it like this, it works perfectly:
<MudTextField Label="search something" Variant="Variant.Text" Clearable="true"
#bind-Value="SearchText" Immediate="true"
#onkeydown="e => KeyboardEventHandler(e)"
/>
#code {
public string SearchText { get; set; }
private void KeyboardEventHandler(KeyboardEventArgs args)
{
if (args.Code == "Escape") { SearchText = string.Empty; }
}
}
This works like a charm. However, I need the component to wait a little while before updating the variable, because each change triggers a new filtering process that may block the UI for a bit (since BlazorWASM, at the time, doesn't support multithreading). So, I use DebounceInterval, i. e. do it like this:
<MudTextField Label="search something" Variant="Variant.Text" Clearable="true"
#bind-Value="SearchText" Immediate="true" DebounceInterval="350"
#onkeydown="e => KeyboardEventHandler(e)"
/>
#code {
public string SearchText { get; set; }
private void KeyboardEventHandler(KeyboardEventArgs args)
{
if (args.Code == "Escape") { SearchText = string.Empty; }
}
}
If I do it like this, when typing faster than the debounce interval, only the last typed character is kept (i.e. every character typed replaces the one before it), as long as the #onkeydown event is used, too.
How can I make this work? I'd be grateful for a possible solution or ideas towards it.
Thanks for taking the time to read this.

There are a few ways to implement a debouncer. The simplest is just to do a backoff of say 300ms before doing your lookup. This gives you a 300ms + your lookup period.
Here's a slightly different method that uses Tasks and waits either the backoff period or the query period whichever is the longer.
The deboucer with lots of comment code to explain what's happening.
public sealed class ActionLimiter
{
private int _backOffPeriod = 0;
private Func<Task> _taskToRun;
private Task _activeTask = Task.CompletedTask;
private TaskCompletionSource<bool>? _queuedTaskCompletionSource;
private TaskCompletionSource<bool>? _activeTaskCompletionSource;
private async Task RunQueueAsync()
{
// if we have a completed task then null it
if (_activeTaskCompletionSource is not null && _activeTaskCompletionSource.Task.IsCompleted)
_activeTaskCompletionSource = null;
// if we have a running task then everything is already in motion and there's nothing to do
if (_activeTaskCompletionSource is not null)
return;
// run the loop while we have a queued request.
while (_queuedTaskCompletionSource is not null)
{
// assign the queued task reference to the running task
_activeTaskCompletionSource = _queuedTaskCompletionSource;
// And release the reference
_queuedTaskCompletionSource = null;
// start backoff task
var backoffTask = Task.Delay(_backOffPeriod);
// start main task
var mainTask = _taskToRun.Invoke();
// await both ensures we run the backoff period or greater
await Task.WhenAll( new Task[] { mainTask, backoffTask } );
// Set the running task completion as complete
_activeTaskCompletionSource.TrySetResult(true);
// and release our reference to the running task completion
// The originator will still hold a reference and can act on it's completion
_activeTaskCompletionSource = null;
// back to the top to check if another task has been queued
}
return;
}
public Task<bool> QueueAsync()
{
var oldCompletionTask = _queuedTaskCompletionSource;
// Create a new completion task
var newCompletionTask = new TaskCompletionSource<bool>();
// get the actual task before we assign it to the queue
var task = newCompletionTask.Task;
// replace _queuedTaskCompletionSource
_queuedTaskCompletionSource = newCompletionTask;
// check if we already have a queued queued task.
// If so set it as completed, false = not run
if (oldCompletionTask is not null && !oldCompletionTask.Task.IsCompleted)
oldCompletionTask?.TrySetResult(false);
// if we don't have a running task or the task is complete , then there's no process running the queue
// So we need to call it and assign it to `runningTask`
if (_activeTask is null || _activeTask.IsCompleted)
_activeTask = this.RunQueueAsync();
// return the reference to the task we queued
return task;
}
private ActionLimiter(Func<Task> toRun, int backOffPeriod)
{
_backOffPeriod = backOffPeriod;
_taskToRun = toRun;
}
/// <summary>
/// Static method to create a new deBouncer
/// </summary>
/// <param name="toRun">method to run to update the component</param>
/// <param name="backOffPeriod">Back off period in millisecs</param>
/// <returns></returns>
public static ActionLimiter Create(Func<Task> toRun, int backOffPeriod)
=> new ActionLimiter(toRun, backOffPeriod > 300 ? backOffPeriod : 300);
}
And a demo page to show how to use it:
#page "/"
#using Blazr.UI
<h3>Test</h3>
<input class="form-control mb-2" type="text" #oninput=OnInput />
<div class="alert alert-info">
#message
</div>
#code {
private ActionLimiter _limiter;
private string? message;
private string? value;
public Test()
=> _limiter = ActionLimiter.Create(this.RunTask, 300);
private async Task OnInput(ChangeEventArgs e)
{
value = e.Value?.ToString() ?? null;
await _limiter.QueueAsync();
}
private async Task RunTask()
{
// simulate doing your search
await Task.Delay(1000);
message = value;
}
}
There's a full CodeProject article here describing this technique and how to build a Typeahead control using it.
https://www.codeproject.com/Articles/5351256/Building-a-Blazor-Autocomplete-Control

Related

Xamarin Android IntentService calling OnDestroy while performing large async operation

Im trying to perform a synchronization task without blocking UI thread. I have implemented a Android Service to do so, but I found out, if the synchronization task needs too much computational time, the UI thread was blocked. So I tried the migration to IntentService. This is how my IntentService looks like:
[Service]
public class SynchronizeIntentService : IntentService
{
static readonly string TAG = typeof(SynchronizeIntentService).FullName;
private NotificationCompat.Builder Builder;
private NotificationManagerCompat NotificationManager;
public SynchronizeIntentService() : base("SynchronizeIntentService")
{
}
public override void OnDestroy()
{
var tmp = 5;
base.OnDestroy();
}
private NotificationChannel createNotificationChannel()
{
var channelId = Constants.NOTIFICATION_CHANNELID;
var channelName = "My Notification Service";
var Channel = new NotificationChannel(channelId, channelName, Android.App.NotificationImportance.Default);
Channel.LightColor = Android.Resource.Color.HoloBlueBright;
Channel.LockscreenVisibility = NotificationVisibility.Public;
return Channel;
}
private void createForegroundService()
{
var mNotificationManager = GetSystemService(Context.NotificationService) as NotificationManager;
if (Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.O)
{
mNotificationManager.CreateNotificationChannel(createNotificationChannel());
}
var notificationBuilder = new NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNELID);
GenerateNotification();
StartForeground(Constants.SERVICE_RUNNING_NOTIFICATION_ID, Builder.Notification);
}
private void GenerateNotification()
{
NotificationManager = NotificationManagerCompat.From(this);
Builder = new NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNELID);
Builder.SetContentTitle(ContaScan.Classes.Localize.GetString("Global_SynchProcess", ""))
.SetSmallIcon(Resource.Drawable.icon)
.SetPriority(NotificationCompat.PriorityLow);
}
protected async override void OnHandleIntent(Intent intent)
{
Log.Debug(TAG, "Service Started!");
await Synch();
Log.Debug(TAG, "Service Stopping!");
StopForeground(true);
this.StopSelf();
}
public override StartCommandResult OnStartCommand(Intent intent, [GeneratedEnum] StartCommandFlags flags, int startId)
{
base.OnStartCommand(intent, flags, startId);
createForegroundService();
return StartCommandResult.Sticky;
}
private async Task Synch()
{
//Large synch process
}
}
And this is how the service is getting started:
startServiceIntent = new Intent(Android.App.Application.Context, typeof(SynchronizeIntentService));
startServiceIntent.SetAction(Constants.ACTION_START_SERVICE);
ContextWrapper contextWrapper = new ContextWrapper(Android.App.Application.Context);
contextWrapper.StartService(startServiceIntent);
The problem is OnDestroy() method is called while the Synch() task is being performed and looks like the IntentService is being killed before ending the process.
What am I doing wrong?
First, check your API level. This class was deprecated in API level 30.
And then, when you use the OnHandleIntent, do not call Service.stopSelf().
This method is invoked on the worker thread with a request to process. Only one Intent is processed at a time, but the processing happens on a worker thread that runs independently from other application logic. So, if this code takes a long time, it will hold up other requests to the same IntentService, but it will not hold up anything else. When all requests have been handled, the IntentService stops itself, so you should not call Service.stopSelf().
For more details, please check the link below. https://developer.android.com/reference/android/app/IntentService#onHandleIntent(android.content.Intent)

apache ignite datastreamer how to set data into ignitefuture?

I am creating a batch data streamer in apache ignite, and need to control what happening after data receive.
My batch has a structure:
public class Batch implements Binarylizable, Serializable {
private String eventKey;
private byte[] bytes;
etc..
Then i trying to stream my data:
try (IgniteDataStreamer<Integer, Batch> streamer = serviceGrid.getIgnite().dataStreamer(cacheName);
StreamBatcher batcher = StreamBatcherFactory.create(event) ){
streamer.receiver(StreamTransformer.from(new BatchDataProcessor(event)));
streamer.autoFlushFrequency(1000);
streamer.allowOverwrite(true);
statusService.updateStatus(event.getKey(), StatusType.EXECUTING);
int counter = 0;
Batch batch = null;
IgniteFuture<?> future = null;
while ((batch = batcher.batch()) != null) {
future = streamer.addData(counter++, batch);
}
Object getted = future.get();
Just for test use lets get only the last future, and try to analyze this object. In the code above I'm using BatchDataProcessor, that look like this:
public class BatchDataProcessor implements CacheEntryProcessor<Integer, Batch, Object> {
private final Event event;
private final String eventKey;
public BatchDataProcessor(Event event) {
this.event = event;
this.eventKey = event.getKey();
}
#Override
public Object process(MutableEntry<Integer, Batch> mutableEntry, Object... objects) throws EntryProcessorException {
Node node = NodeIgniter.node(Ignition.localIgnite().cluster().localNode().id());
ServiceGridContainer container = (ServiceGridContainer) node.getEnvironmentContainer().getContainerObject(ServiceGridContainer.class);
ProcessMarshaller marshaller = (ProcessMarshaller) container.getService(ProcessMarshaller.class);
LocalProcess localProcess = marshaller.intoProccessing(event.getLambdaExecutionKey());
try {
localProcess.addBatch(mutableEntry);
} catch (IOException e) {
e.printStackTrace();
} finally {
return new String("111");
}
}
}
So after localProcess.addBatch(mutableEntry) I want to send back an information about the status of this particular batch, so I think that I should do this in IgniteFuture object, but I don't find any information how to control the future object that's received in addData function.
Can anybody help with understanding, where can I control future that receives in addData function or some other way to realize a callback to streamed batch?
When you do StreamTransformer.from(), you forfeit the result of your BatchDataProcessor, because
for (Map.Entry<K, V> entry : entries)
cache.invoke(entry.getKey(), this, entry.getValue());
// ^ result of cache.invoke() is discarded here
DataStreamer is for one-directional streaming of data. It is not supposed to return values as far as I know.
If you depend on the result of cache.invoke(), I recommend calling it directly instead of relying on DataStreamer.
BTW, be careful with fut.get(). You should do dataStreamer.flush() first, or DataStreamer's futures will wait indefinitely.

How to stop current job and start new job use quartz.net in asp.net mvc?

I've made a crawler on a website it has to read a website and fetch some values from it website.I've made use quartz.net and Asp.net MVC. but what is my problem? in fact,My problem is that for example,he/she the first time start for scraping a "Stackoverflow.com" about 5 hours and then he/she is decided stop "stackoverflow.com" and start a scrap new website.So,How can i do it?
[HttpPost]
public ActionResult Index(string keyword, string url)
{
IScheduler scheduler = StdSchedulerFactory.GetDefaultScheduler();
scheduler.Start();
IJobDetail job = JobBuilder.Create<ScrapJob>()
.WithIdentity("MyScrapJob")
.UsingJobData("url", url)
.UsingJobData("keyword", keyword)
.Build();
ITrigger trigger = TriggerBuilder.Create().WithDailyTimeIntervalSchedule(
s => s.WithIntervalInSeconds(20).OnEveryDay().StartingDailyAt(TimeOfDay.HourAndMinuteOfDay(0, 0))
).Build();
scheduler.ScheduleJob(job, trigger);
return View(db.Scraps.ToList());
}
public List<ScrapJob> Scraping(string url, string keyword)
{
int count = 0;
List<ScrapJob> scraps = new List<ScrapJob>();
ScrapJob scrap = null;
HtmlDocument doc = new HtmlDocument();
try
{
var request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "GET";
using (var response = (HttpWebResponse)request.GetResponse())
{
using (var stream = response.GetResponseStream())
{
doc.Load(stream, Encoding.GetEncoding("UTF-8"));
foreach (HtmlNode node in doc.DocumentNode.SelectNodes("//text()"))
{
if (node.InnerText.ToString().Contains(keyword))
{
count++;
scrap = new ScrapJob { Keyword = keyword, DateTime = System.DateTime.Now.ToString(), Count = count, Url = url };
}
}
}
}
}
catch (WebException ex)
{
Console.WriteLine(ex.Message);
}
// scraps.Add(scrap);
var isExist = db.Scraps.Where(s => s.Keyword == keyword && s.Count == scrap.Count).Max(s => (int?)s.Id) ?? 0;
if (isExist == 0)
{
db.Scraps.Add(scrap);
db.SaveChanges();
}
return scraps;
}
public void Execute(IJobExecutionContext context)
{
//ScrapJob scraps = null;
using (var scrap = new ScrapJob())
{
JobKey key = context.JobDetail.Key;
JobDataMap dataMap = context.JobDetail.JobDataMap;
string url = dataMap.GetString("url");
string keyword = dataMap.GetString("keyword");
scrap.Scraping(url, keyword);
}
}
I'm not sure why you picked QUARTZ, but here is something that I think will help you.
This is a code sample that interrupt and delete job by unique identifier
public void DeleteJob(JobKey jobKey)
{
var scheduler = StdSchedulerFactory.GetDefaultScheduler();
var executingJobs = scheduler.GetCurrentlyExecutingJobs();
if (executingJobs.Any(x => x.JobDetail.Key.Equals(jobKey)))
{
scheduler.Interrupt(jobKey);
}
scheduler.DeleteJob(jobKey);
}
But I believe you need to define what behavior you expect, because it can be a bit more complex for example:
If you like to just pause the job and resume it after finish with the other website /persist some state and progress/ or just log the progress
If you want them to run in parallel and process multiple sites simultaneously. (You just need to give different names instead of the hardcoded .WithIdentity("MyScrapJob") )
Also with scheduler.GetCurrentlyExecutingJobs() you can get the currently executing jobs, show them to the user and let him decide what to do.
Also looking at your action method I'm not sure whether this is the behavior you expect of that trigger. Also what bothers me is db.Scraps.ToList() you will materialize the whole table you can consider adding pagination as well in your case is not necessary because you will only show count but its mandatory if you have a lot of records in the grid.
About the scraping method
Instead of
var isExist = db.Scraps.Where(s => s.Keyword == keyword && s.Count == scrap.Count).Max(s => (int?)s.Id) ?? 0;
you can use .Any
var exists = db.Scraps.Any(s => s.Keyword == keyword && s.Count == scrap.Count);
this will return boolean and you can check if(!exists)
You can check https://github.com/AngleSharp/AngleSharp it's high performance web parsing library. Super easy to use as well.
I see possibility of duplicated records by keyword if you check them by keyword and count - not sure whether you want this or just want to update the existing record with it's counter
Good luck! I hope this answer helps you :)

Selenium2 WebDriver (Page factory) error after a postback (Element not found in the cache)

I'm using Selenium 2 tests (written in C#) that choose values from a "select" control.
Selection causes a post-back to the server, which updates the state of the page.
It s really frustrating because every PostBack occurs this exception
Element not found in the cache - perhaps the page has changed since it was looked up
Just to be precise i use Selenium 2 WebDriver (2.32.0.0)
And for my project i Use Pattern Page Factory
The code looks like that
class RegisterPersonelData
{
private IWebDriver driver;
[FindsBy(How = How.Id, Using = "ctl00_ContentMain_register1_txtName")]
private IWebElement txtLastname;
[FindsBy(How = How.Id, Using = "ctl00_ContentMain_register1_lstDrvLic")]
private IWebElement dlDrive;
private SelectElement selectDrive;
[FindsBy(How = How.Id, Using = "ctl00_ContentMain_register1_lstVeh")]
private IWebElement dlVehicule;
private SelectElement selectVehicule;
public RegisterPersonelData(IWebDriver driver)
{
this.driver = driver;
// initialize elements of the LoginPage class
PageFactory.InitElements(driver, this);
// all elements in the 'WebElements' region are now alive!
// FindElement or FindElements no longer required to locate elements
}
public void fillData(string lastname, string drive, string vehicule)
{
txtLastname.SendKeys(lastname);
this.selectDrive = new SelectElement(dlDrive);
selectDrive.SelectByText(drive);
selectVehicule = new SelectElement(dlVehicule);
IWait<IWebDriver> wait = new WebDriverWait(this.driver, TimeSpan.FromSeconds(Convert.ToInt32(ConfigurationManager.AppSettings["ExpliciteWait"])));
wait.Until(x => selectVehicule.Options.Count > 1);
selectVehicule.SelectByText(vehicule);
}
}
And here the code of main
class Program
{
static void Main()
{
IWebDriver driver = MyWebDriver.GetWebDriver(MyWebDriver.BrowserType.FIFREFOX);
driver.Navigate().GoToUrl("http://...");
...
registerPersonelData.fillData("lastname", "Permis B", "Auto");
}
}
This code doesn t work because one postback is triggered ...
I have try to use one explicite wait but it fails too !
Code use to retrieve one element with explicite wait
public static IWait<IWebDriver> GetWaitWebDriver(IWebDriver driver)
{
IWait<IWebDriver> wait = new WebDriverWait(driver, TimeSpan.FromSeconds(Convert.ToInt32(ConfigurationManager.AppSettings["ExpliciteWait"])));
return wait;
}
public static IWebElement GetElementAndWaitForIt(IWebDriver driver, By by)
{
return GetWaitWebDriver(driver).Until(x =>
{
return x.FindElement(by);
});
}
Someone has one idea to fix it ?
You can re-initialize the elements at the end of the fillData method.
PageFactory.InitElements(driver, this);
You could do it in the main as well with:
PageFactory.InitElements(driver, registerPersonelData);
You could also try adding the following to the field you need to reuse.
[CacheLookup]
I have done a lot of tries and finally i have found something usefull
public static void WaitForAnWebElementDisplayed(IWait<IWebDriver> wait, IWebElement webElement)
{
wait.Until(x => webElement.Displayed);
}
public static void WaitForAnWebElementEnabled(IWait<IWebDriver> wait, IWebElement webElement)
{
wait.Until(x => webElement.Enabled);
}
and Consequently i can wait that the load of page triggered after to have choosen one item in select option is completed !

ContentProvider: How to cancel a previous call to delete()?

I'm using a custom ContentProvider. For querying, there is a CancellationSignal (API 16+) which can be used to cancel a previous call to query().
My question: How can I archive that with delete()? For clarification, my custom provider manages files on SD card, and so I want to be able to cancel delete operation inside my provider.
I solved this with a simple solution.
For example, for every call to query(), we put a parameter pointing to the task ID, and use a SparseBooleanArray to hold that ID, like:
...
private static final SparseBooleanArray _MapInterruption = new SparseBooleanArray();
...
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
Cursor cursor = ...
int taskId = ... // obtain task ID from uri
boolean cancel = ... // obtain cancellation flag from uri
if (cancel) {
_MapInterruption.put(taskId, true);
return null; // don't return any cursor
} else {
doQuery(taskId);
if (_MapInterruption.get(taskId)) {
_MapInterruption.delete(taskId);
return null; // because the task was cancelled
}
}
...
return cursor;
}// query()
private void doQuery(int taskId) {
while (!_MapInterruption.get(taskId)) {
... // do the task here
}
}// doQuery()
Usage:
To query:
...
getContentResolver().query("content://your-uri?task_id=1", ...);
To cancel:
...
getContentResolver().query("content://your-uri?task_id=1&cancel=true", ...);
For a complete working solution, have a look at android-filechooser.
The advantage is you can use this technique in Android… 1+ and for other methods such as delete(), update()... While CancellationSignal is only available in API 16+ and is limited to only query().

Resources