Stopping Background Task On Page Navigation Back - c#

I'm working on a Xamarin.Forms project that supports iOS and Android devices, and I'm using the MVVM design pattern.
I have navigation root page that consists of a ListView, when item is selected on this ListView, I execute the following command to Navigate to item details view.
Page DetailsPage = new View.DetailsView(SelectedItemData);
await Navigation.PushAsync(DetailsPage);
Once this Details Page is opened, I start running a background task.
private void StartBackgroundTask(){
TimerBackgroundTask = new Timer((o) => {
Device.BeginInvokeOnMainThread(() => Update()); }, null, 0, 1000);
}
}
Which is based on this class
public class Timer : CancellationTokenSource
{
public bool IsDisposed { get; set; }
public Timer(Action<object> callback, object state, int dueTime, int period)
{
System.Threading.Tasks.Task.Delay(dueTime, Token).ContinueWith(async (t, s) =>
{
Tuple<Action<object>, object> tuple = (Tuple<Action<object>, object>)s;
while (!IsCancellationRequested)
{
await System.Threading.Tasks.Task.Run(() => tuple.Item1(tuple.Item2));
await System.Threading.Tasks.Task.Delay(period);
}
},
Tuple.Create(callback, state), CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously |
TaskContinuationOptions.OnlyOnRanToCompletion,
TaskScheduler.Default);
}
protected override void Dispose(bool disposing)
{
IsDisposed = true;
if (disposing)
{
Cancel();
}
base.Dispose(disposing);
}
}
Update function updates UI every 1 second.
Everything works fine and as it should, no issues here, however problems start to occur once I navigate back to root page, and back to details page - doing so twice causes the following error:
System.ArgumentException'jobject' must not be IntPtr.Zero. Parameter name: jobject
The problem stops occurring once the StartBackgroundTask gets disabled entirely from the code, so I believe that it is the one responsible for the error. Furthermore, I'm fairly convinced that this background task keeps on running somewhere in the thread even though I navigate back to the root page and I believe that if I could somehow dispose of the background task OnDissapearing event / navigation back button pressed, the error would no longer persist.
Unfortunately I have no idea how I how or even if its possible to somehow bind command to navigation back pressed event given my Views are bound to ViewModel.
Any tips would be greatly appreciated.

You can detect that a page is being dismissed by overriding OnDisappearing. In your DetailPage you could have something like this:
protected override void OnDisappearing()
{
TimerBackgroundTask?.Dispose();
base.OnDisappearing();
}

Related

Xamarin Forms navigation not working as expected in debug mode

We are working on a xamarin forms app where INavigationService is used for navigating between pages. I found a strange observation that i haven't come across.
Observation 1: App is deployed in Emulator/simulator and debug mode is stopped.
=> Navigation is working as expected
Observation 2: App is tested while running i debug mode.
=> Navigation to few screens not happening until user clicks randomly on screen. i.e, when user navigates from ClassA to ClassB the control(code execution) is shifted from ClassA to ClassB but UI still shows ClassA and when user clicks on screen randomly it navigates to ClassB UI ,the method in which the Navigation code is written in ClassA doesn’t end until user click on screen.
Code:
Class A VM:
private async Task ContinueToNextPageCommandAsync()
{
IsBusy= true;
if (listofItems <= 0)
{
await DialogService.ShowAlertAsync("Warning");
return;
}
await NavigationService.NavigateToAsync(typeof(ClassBViewModel), parameter);
IsBusy= False;
return;
}
Class B VM:
public override async Task InitializeAsync(object navigationData1,
object navigationData2 = null, object navigationData3 = null)
{
try
{
IsLoadingText = true;
// Statements to be executed during class initiation
IsLoadingText = false;
}
}
Note: IsBusy=False in classA doesn’t gets triggered until user clicks on screen after navigation.

State is wiped out when page is refresh in blazor with fluxor

I am not sure how to handle this behavior, this is the Feature I have:
public class HolderFeature : Feature<HolderState>
{
public override string GetName() => "HolderState";
protected override HolderState GetInitialState() => new(holder: new ProductHolder(), persons: string.Empty);
}
And, everything works fine until a page is refreshed (pressing f5), the state is wiped out, I have this to handle the error:
protected override void OnInitialized()
{
if (HolderState.Value.QuotedProduct.Quotes != null)
{
//do the logic
}
else
{
//show error screen
PressedF5 = true;
}
}
What I expect is that even when the page is refreshed it shouldn't wipe out the state. how can I do that?
The state is stored in memory. When you refresh the page you are unloading it and starting from scratch, so all the memory is lost if it isn't persisted somewhere.

Blazor components: notify of collection-changed event causing thread collisions

I am working on an ASP.NET Core Blazor application with .Net Core 3.0 (I am aware of 3.1, but due to Mordac I am stuck with this version for now).
I have a multiple-component page, and some of those components require access to the same data and need to all be updated when the collection is updated. I've been trying to use EventHandler-based callbacks, but those get invoked on their own threads at about the same time (if I understand correctly), causing the callbacks in the .razor components to attempt to make service calls to the context at the same time.
Note: I've tried making my DbContext`s lifetime transient, but I'm still getting the race conditions.
It's quite possible that I gotten myself into an async blender and don't know how to get out.
I've tentatively concluded that the event EventHandler methodology will not work here. I need some way to trigger "collection changed" updates to the components without triggering a race condition.
I've thought about updating the services involved in these race conditions with the following:
Replace every search function with a publically bindable collection property
Having every create/update/delete call update every single one of these collections
This would allow the components to bind directly to the collections that are changed, which I think will cause every binding to it in any component to update without the needing to be explicitly told, and this in turn would allow me to ditch the "collection changed" event handling entirely.
But I'm hesitant to try this and haven't done it yet because it would introduce a fair amount of overhead on each major service function.
Other ideas? Please help. If a collection has changed, I want Blazor components that rely on that collection to somehow be able to update, whether through notifications or binding or some other way.
The following code is a heavy simplification of what I've got, and it's still causing race conditions when the event handlers are invoked from the service.
Model
public class Model
{
public int Id { get; set; }
public string Msg { get; set; }
}
MyContext
public class MyContext : DbContext
{
public MyContext() : base()
{
Models = Set<Model>();
}
public MyContext(DbContextOptions<MyContext> options) : base(options)
{
Models = Set<Model>();
}
public DbSet<Model> Models { get; set; }
}
ModelService
public class ModelService
{
private readonly MyContext context;
private event EventHandler? CollectionChangedCallbacks;
public ModelService(MyContext context)
{
this.context = context;
}
public void RegisterCollectionChangedCallback(EventHandler callback)
{
CollectionChangedCallbacks += callback;
}
public void UnregisterCollectionChangedCallback(EventHandler callback)
{
CollectionChangedCallbacks -= callback;
}
public async Task<Model[]> FindAllAsync()
{
return await Task.FromResult(context.Models.ToArray());
}
public async Task CreateAsync(Model model)
{
context.Models.Add(model);
await context.SaveChangesAsync();
// No args necessary; the callbacks know what to do.
CollectionChangedCallbacks?.Invoke(this, EventArgs.Empty);
}
}
Startup.cs (excerpt)
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
string connString = Configuration["ConnectionStrings:DefaultConnection"];
services.AddDbContext<MyContext>(optionsBuilder => optionsBuilder.UseSqlServer(connString), ServiceLifetime.Transient);
services.AddScoped<ModelService>();
}
ParentPage.razor
#page "/simpleForm"
#using Data
#inject ModelService modelService
#implements IDisposable
#if (AllModels is null)
{
<p>Loading...</p>
}
else
{
#foreach (var model in AllModels)
{
<label>#model.Msg</label>
}
<label>Other view</label>
<ChildComponent></ChildComponent>
<button #onclick="(async () => await modelService.CreateAsync(new Model()))">Add</button>
}
#code {
private Model[] AllModels { get; set; } = null!;
public bool ShowForm { get; set; } = true;
private object disposeLock = new object();
private bool disposed = false;
public void Dispose()
{
lock (disposeLock)
{
disposed = true;
modelService.UnregisterCollectionChangedCallback(CollectionChangedCallback);
}
}
protected override async Task OnInitializedAsync()
{
AllModels = await modelService.FindAllAsync();
modelService.RegisterCollectionChangedCallback(CollectionChangedCallback);
}
private void CollectionChangedCallback(object? sender, EventArgs args)
{
// Feels dirty that I can't await this without changing the function signature. Adding async
// will make it unable to be registered as a callback.
InvokeAsync(async () =>
{
AllModels = await modelService.FindAllAsync();
// Protect against event-handler-invocation race conditions with disposing.
lock (disposeLock)
{
if (!disposed)
{
StateHasChanged();
}
}
});
}
}
ChildComponent.razor
Copy-paste (for the sake of demonstration) of ParentPage minus the label, ChildComponent, and model-adding button.
Note: I've also experimented with attempting to insert a block of code into the HTML portion of the component, but that didn't work either since I can't use an await there.
Possibly bad idea that I experimented with (and that still didn't avoid the threading collision):
#if (AllModels is null)
{
<p><em>Loading...</em></p>
#Load();
#*
Won't compile.
#((async () => await Load())());
*#
}
else
{
...every else
}
#code {
...Initialization, callbacks, etc.
// Note: Have to return _something_ or else the #Load() call won't compile.
private async Task<string> Load()
{
ActiveChargeCodes = await chargeCodeService.FindActiveAsync();
}
}
Please help. I'm experimenting in (for me) uncharted territory.
Since i'm currently in a situation that looks awfully lot like yours, let me share what i found out. My issue was "StateHasChanged()". Since i've seen that call in your code too, maybe the following helps:
i got a pretty simple callback handler:
case AEDCallbackType.Edit:
// show a notification in the UI
await ShowNotification(new NotificationMessage() { Severity = NotificationSeverity.Success, Summary = "Data Saved", Detail = "", Duration = 3000 });
// reload entity in local context to update UI
await dataService.ReloadCheckAfterEdit(_currentEntity.Id);
the notification function does this:
async Task ShowNotification(NotificationMessage message)
{
notificationService.Notify(message);
await InvokeAsync(() => { StateHasChanged(); });
}
the reload function does this:
public async Task ReloadCheckAfterEdit(int id)
{
Check entity = context.Checks.Find(id);
await context.Entry(entity).ReloadAsync();
}
The problem was the StateHasChanged() call. It tells the UI to re-render. The UI consists of a datagrid component. The datagrid calls a query in the dataservice, to fetch data from the DB.
This happens just right before "ReloadAsync" is called, which is "awaited". Once ReloadAsync actually executes, it happens in a different thread, causing the dreaded "A second operation started on this context before a previous operation completed" exception.
My Solution was to remove the StateHasChanged line completely from where it was, and call it once after everything else was completed. No more concurrent caller issues.
Good luck solving this, i feel your pain.

Asynchronous data loading and subsequent error handling

I have an application that involves a database. Previously, upon opening a window, I would query the database and use this to populate aspects of my view model. This worked reasonably well, but could create noticeable pauses when the data access took longer than expected.
The natural solution, of course, is to run the database query asynchronously and then populate the view model when that query completes. This isn't too hard, but it raises some interesting questions regarding error handling.
Previously, if something went wrong with the database query (a pretty big problem, granted), I would propagate the exception through the view model constructor, ultimately making it back up to the caller that wanted to open the window. It could then display an appropriate error and not actually open the window.
Now, however, the window opens right away, then populates later as the query completes. The question, now, is at what point should I check for an error in the background task? The window is already open, so the behavior needs to be different somehow, but what is a clean way to indicate the failure to the user and allow for graceful recovery/shutdown?
For reference, here is a snippet demonstrating the basic pattern:
public ViewModel()
{
_initTask = InitAsync();
//Now where do I check on the status of the init task?
}
private async Task InitAsync()
{
//Do stuff...
}
//....
public void ShowWindow()
{
var vm = new ViewModel(); //Previously this could throw an exception that would prevent window from being shown
_windowServices.Show(vm);
}
One option I've considered is use an asynchronous factory method for constructing the ViewModel, allowing the entire thing to be constructed and initialized before attempting to display the window. This preserves the old approach of reporting errors before the window is ever opened. However, it gives up some of the UI responsiveness gained by this approach, which allows initial loading of the window to occur in parallel with the query and also allows me (in some cases) to update the UI in increments as each query completes, rather than having the UI compose itself all at once. It avoids locking up the UI thread, but it doesn't reduce the time before the user actually sees the window and can start interacting with it.
Maybe use some kind of messaging/mediator between your viewmodel and underlying service?
Semi-pseudo code using MVVMLight
public ViewModel()
{
Messenger.Default.Register<NotificationMessage<Exception>>(this, message =>
{
// Handle here
});
Task.Factory.StartNew(() => FetchData());
}
public async Task FetchData()
{
// Some magic happens here
try
{
Thread.Sleep(2000);
throw new ArgumentException();
}
catch (Exception e)
{
Messenger.Default.Send(new NotificationMessage<Exception>(this, e, "Aw snap!"));
}
}
I dealt with a similar problem here. I found it'd be best for me to raise an error event from inside the task, like this:
// ViewModel
public class TaskFailedEventArgs: EventArgs
{
public Exception Exception { get; private set; }
public bool Handled { get; set; }
public TaskFailedEventArgs(Exception ex) { this.Exception = ex; }
}
public event EventHandler<TaskFailedEventArgs> TaskFailed = delegate { };
public ViewModel()
{
this.TaskFailed += (s, e) =>
{
// handle it, e.g.: retry, report or set a property
MessageBox.Show(e.Exception.Message);
e.Handled = true;
};
_initTask = InitAsync();
//Now where do I check on the status of the init task?
}
private async Task InitAsync()
{
try
{
// do the async work
}
catch (Exception ex)
{
var args = new TaskFailedEventArgs(ex);
this.TaskFailed(this, args);
if (!args.Handled)
throw;
}
}
// application
public void ShowWindow()
{
var vm = new ViewModel(); //Previously this could throw an exception that would prevent window from being shown
_windowServices.Show(vm);
}
The window still shows up, but it should be displaying some kind of progress notifications (e.g. using IProgress<T> pattern), until the end of the operation (and the error info in case it failed).
Inside the error event handler, you may give the user an option to retry or exit the app gracefully, depending on your business logic.
Stephen Cleary has a series of posts on his blog about Async OOP. In particular, about constructors.

MvvmCross and view navigation

I'm using MvvmCross for my application and starting with iPhone. To achieve the sophistication on navigating between views using RequestNavigate and Close I've derived a new presenter from MvxBaseTouchViewPresenter. So far so good.
The problem I'm having now is that it is possible to close or show a view while the previously visible view is still in transition, either opening or closing. The only way I could think of to work round this problem was to use the ViewDidAppear and ViewDidDisappear event handlers of view controllers to call an Action that does the showing of the next view thereby deferring the showing until after the previous view has finished its transition.
This means I have to add:
public override void ViewDidDisappear(bool animated)
{
base.ViewDidDisappear(animated);
DoPostTransitionAction();
}
public override void ViewDidAppear (bool animated)
{
base.ViewDidAppear (animated);
DoPostTransitionAction();
}
private void DoPostTransitionAction()
{
if( _postTransitionAction != null)_postTransitionAction();
}
private Action _postTransitionAction;
public void ShowActionAfterTransition( Action postTransitionAction)
{
if( this.IsBeingDismissed || this.IsBeingPresented)
{
_postTransitionAction = postTransitionAction;
}
else
{
_postTransitionAction = null;
postTransitionAction();
}
}
to view controllers, introduce an IEventViewController interface to make me add the code and change the Show method of the presenter to be:
public override void Show(MvxShowViewModelRequest request)
{
// find the current top view as it will disappear as a result of
// showing the requested view
// note: this will be null for the first show request
var topViewController = TopViewController;
// if the request is to roll back an existing stack of views
// then rollback to the bottom of the stack
if (request.ClearTop && topViewController != null)
{
RollBackStack(topViewController);
}
// the top view is about to be 'covered' by the view requested
// if the top view is transitioning (appearing or disappearing)
// then wait for it to finish before showing the view requested
var disappearingViewController = topViewController as IEventViewController;
// if the top view is disappearing and it's of the 'event' type
// delay calling Show until after the view has disappeared
if( disappearingViewController != null)
{
// the top view has been requested to close and will fire the
// 'transition complete' event when it has
// register the action to perform when the top view has disappeared
disappearingViewController.ShowActionAfterTransition (() =>
{
Show (CreateView(request), true);
});
}
else
{
// show the view now as the top view is either not closing
// or, if it is, is not going to say when it does
// create a view controller of the type requested
Show(CreateView(request), true);
}
}
What I'd really like to have would be those event handlers in the base class. I wonder what are the chances without me setting myself adrift from the main development path? Partial classes might do it?

Categories

Resources