Blazor WASM - Timer stops after some time [duplicate] - c#

I have written a program that updates pages regularly with the Blazor server app.
When this program is executed, the current time and the value from the database table are acquired every second and displayed on the page, but the processing in this timer stops after a certain amount of time.
Although it is not measured accurately, the processing in the timer stops after about 10 to 30 minutes.
However, when I look at the debug tool of the browser, there is no error, and there is no error on visual studio.
I searched for an example like this but couldn't find it and couldn't determine what was causing it.
I want the processing in this timer to work until I explicitly specify it as stopped.
What could be the cause of this?
I'm in trouble because I don't know the cause.
Please give me some advice.
Thank you.
private string Time { get; set; }
protected override void OnInitialized()
{
var timer = new System.Threading.Timer((_) =>
{
Time = DateTime.Now.ToString();
InvokeAsync(() =>
{
// Get the value from the database
databaseValue = TimerProcessGetValue();
StateHasChanged();
});
}, null, 0, 1000);
base.OnInitialized();
}
Process called in Timer
public async Task<int?> TimerProcessGetValue()
{
int? timerProcessValue;
using(DbContext = DbFactory.CreateDbContext())
{
timerProcessValue = dbContext.TestTable.Single(x => x.Id == 1).TestValue;
}
return timerProcessValue;
}
postscript
The full text of the target component.
PageRefleshTest.cs
#page "/refresh-ui-manually"
#using System.Threading;
#using TestBlazorServer.Models;
#using Microsoft.EntityFrameworkCore;
#inject IDbContextFactory<SQLbeginnerContext> DbFactory
<h1>#Time</h1>
<h1>TestValue:#databaseValue</h1>
private string Time { get; set; }
private int? databaseValue { get; set; }
protected override void OnInitialized()
{
var timer = new System.Threading.Timer((_) =>
{
Time = DateTime.Now.ToString();
InvokeAsync(() =>
{
// Get the value from the database
databaseValue = TimerProcessGetValue().Result;
StateHasChanged();
});
}, null, 0, 1000);
base.OnInitialized();
}
public async Task<int?> TimerProcessGetValue()
{
int? timerProcessValue;
using (var dbContext = DbFactory.CreateDbContext())
{
timerProcessValue = dbContext.TestTable.Single(x => x.Id == 1).TestValue;
}
return timerProcessValue;
}
Implemented using System.Timers.Timer
#page "/Refresh2"
#inject IDbContextFactory<SQLbeginnerContext> DbFactory
#using TestBlazorServer.Models
#using Microsoft.EntityFrameworkCore
#using System.Timers
#implements IDisposable
<h3>PageRefresh2</h3>
<h1>#Time</h1>
<h1>#currentCount</h1>
<h1>TestValue:#databaseValue/h1>
#code {
private int currentCount = 0;
private Timer timer2 = new(1000);
private string Time { get; set; }
private int? databaseValue { get; set; }
protected override void OnInitialized()
{
timer2.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer2.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
currentCount++;
Time = DateTime.Now.ToString();
databaseValue = TimerProcessGetValue().Result;
StateHasChanged();
});
}
public void Dispose() => timer2.Dispose();
public async Task<int?> TimerProcessGetValue()
{
int? timerProcessValue;
await using (var dbContext = DbFactory.CreateDbContext())
{
timerProcessValue = dbContext.TestTable.Single(x => x.Id == 1).TestValue;
}
return timerProcessValue;
}
}

It could of course be from the SignalR connection failing. That should reconnect however.
The main suspect here is .Result. You should always avoid .Wait() and .Result in async code. Aslo, your DB method is not really async, that might play a role too.
But that 'half hour' could also come from the Garbage collector throwing out the Timer object. See the purple Note on this page. When you have a Timer you should anchor it in a field and Dispose() it.
The next step is to make the eventhandler an async void, this is one of the very few cases that is called for. And then only Invoke the StatehasChanged call.
#implements IDisposable
...
#code {
System.Threading.Timer _timer; // use a private field
protected override void OnInitialized()
{
_timer = new System.Threading.Timer(async (_) =>
{
Time = DateTime.Now.ToString();
databaseValue = await TimerProcessGetValue();
await InvokeAsync(StateHasChanged);
}, null, 0, 1000);
// base.OnInitialized(); -- not needed
}
public void Dispose()
{
_timer?.Dispose();
}
}
And make the Db action actually async:
public async Task<int?> TimerProcessGetValue()
{
int? timerProcessValue;
using (var dbContext = DbFactory.CreateDbContext())
{
timerProcessValue =
(await dbContext.TestTable.SingleAsync(x => x.Id == 1))
.TestValue;
}
return timerProcessValue;
}
Only you can test this in-situ, let us know if it works.

Related

StateHasChanged() does not update database values

Using the code you posted in your new answer. But same error when I add a file and the method UpdateVergadering is called.
vergaderingRepository:
private readonly IDbContextFactory<ApplicationDbContext> _factory;
public VergaderingRepository(IDbContextFactory<ApplicationDbContext> dbContextFactory, IDbContextFactory<ApplicationDbContext> factory)
{
_factory = factory;
}
public async ValueTask<int> UpdateVergadering(Vergadering vergadering)
{
using var dbContext = _factory.CreateDbContext();
dbContext.Set<Vergadering>().Update(vergadering);
return await dbContext.SaveChangesAsync();
}
public async ValueTask<Vergadering> GetVergaderingVoorLiveNotulenAsync (int vergaderingId)
{
using var dbContext = _factory.CreateDbContext();
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
return await dbContext.Set<Vergadering>().SingleOrDefaultAsync(x => x.Id == vergaderingId);
}
The error I get:
System.InvalidOperationException: 'The instance of entity type 'Bestuurslid' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
You code never completes the component render, you loop within OnInitialized. You also confuse OnInitialized and OnInitializedAsync.
Here's a demo page that shows how to use the System.Timers.Timer with an event handler hooked up to the timer to handle the data get and UI update. OnInitializedAsync does the initial data get, sets up the timer, wires up the event handler and completes.
#page "/"
#implements IDisposable
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<div class="alert alert-success">
#_message
</div>
#code {
private string? _message = "Not Set";
private System.Timers.Timer _timer = new System.Timers.Timer(2000);
protected async override Task OnInitializedAsync()
{
// Initial data get
_message = await GetData();
// set uo the timer and hook up the event handler
_timer.AutoReset = true;
_timer.Elapsed += this.OnTimerElapsed;
_timer.Start();
}
// Event handler for the timer
private async void OnTimerElapsed(object? sender, System.Timers.ElapsedEventArgs e)
{
_message = await GetData();
// must call this like this as the timer may be running on a different thread
await this.InvokeAsync(StateHasChanged);
}
private async ValueTask<string> GetData()
{
// emulate an async call to a Db or API
await Task.Delay(100);
return DateTime.Now.ToLongTimeString();
}
// Dispose of the event handler when the Renderer has finished with the component
public void Dispose()
=> _timer.Elapsed -= this.OnTimerElapsed;
}
Update on DbContexts and Async behaviour
Set up a DbContextFactory:
services.AddDbContextFactory<MyDbContext>(
options =>
options.UseSqlServer(#"Server=(localdb)\mssqllocaldb;Database=Test"));
And then use the factory to get Db context instances as you need them.
public sealed class MeetingBroker
{
private readonly IDbContextFactory<MyDbContext> _factory;
public MeetingBroker(IDbContextFactory<MyDbContext> factory)
{
_factory = factory;
}
public ValueTask<Vergadering> GetVergaderingByIdAsync(int vergaderingId)
{
using var dbContext = _factory.CreateDbContext();
// if you aren't editing the data then you don't need tracking. Imporves performance
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
return await dbContext.Set<TRecord>().SingleOrDefaultAsync(x => x.Id == vergaderingId));
}
DbContextFctory Update
You've implemented the factory, but not the "Unit of Work" pattern. You're implementation uses the same context for all activity within the repository and will cause usage clashes.
Blazor lives in an async world so you need to code for situations where you have parallel processes running on the same resources.
Your Repository Pattern should look like this:
private readonly IDbContextFactory<ApplicationDbContext> _factoy;
public VergaderingRepository(IDbContextFactory<ApplicationDbContext> dbContextFactory)
{
// assigned the factory not a context
_factory = dbContextFactory;
}
public async ValueTask<Vergadering> GetVergaderingVoorLiveNotulenAsync (int vergaderingId)
{
// creates a context for each transaction
using dbContext = dbContextFactory.CreateDbContext();
return await dbContext.Set<Vergadering>().SingleOrDefaultAsync(x => x.Id == vergaderingId);
}
private readonly IDbContextFactory<ApplicationDbContext> _factory;
public VergaderingRepository(IDbContextFactory<ApplicationDbContext> dbContextFactory, IDbContextFactory<ApplicationDbContext> factory)
=> _factory = factory;
public async ValueTask<Vergadering> GetVergaderingVoorLiveNotulenAsync (int vergaderingId)
{
using var dbContext = _factory.CreateDbContext();
// Turning off tracking as this is only a query.
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
return await dbContext.Set<Vergadering>().SingleOrDefaultAsync(x => x.Id == vergaderingId);
}
public async ValueTask<int> UpdateVergadering(Vergadering vergadering)
{
using var dbContext = _factory.CreateDbContext();
// Tracking is required for updates
//dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
dbContext.Set<Vergadering>().Update(vergadering);
return await dbContext.SaveChangesAsync();
}

Blazor Server System.ObjectDisposedException issues when updating pages

Added the process to update the data every second in Blazor Server.
This works fine, but when I press refresh page (F5) in my browser I get the following error:
System.ObjectDisposedException: 'Cannot process pending renders after the renderer has been disposed.
ObjectDisposed_ObjectName_Name'
The target code is here
#code {
private List<Models.Recipe> recipes { get; set; }
private List<Models.NowOrder> nowOrders { get; set; }
private List<Models.PlanOrder> planOrders { get; set; }
System.Threading.Timer _timer;
protected override void OnInitialized()
{
using (var dbContext = DbFactory.CreateDbContext())
{
this.recipes = dbContext.Recipes.ToList();
this.planOrders = dbContext.PlanOrders.ToList();
this.nowOrders = dbContext.NowOrders.ToList();
}
_timer = new System.Threading.Timer(async (_) =>
{
Time = DateTime.Now.ToString();
databaseValue = await TimerProcessGetValue();
await InvokeAsync(StateHasChanged);
}, null, 0, 1000);
}
public void Dispose()
{
_timer?.Dispose();
}
public async Task<int?> TimerProcessGetValue()
{
int? timerProcessValue;
using (var dbContext = DbFactory.CreateDbContext())
{
timerProcessValue = (await dbContext.TestTable.SingleAsync(x => x.Id == 1)).TestValue;
}
return timerProcessValue;
}
}
When refreshing the page "await InvokeAsync(StateHasChanged);"
If you comment out the following part, you will not get an error even if you press the F5 key, so I think that my handling of asynchronous processing is wrong, but I am troubled because I can not find the same case even if I search for the error I am.
What do you think is the cause of this?
Thank you for your cooperation.
_timer = new System.Threading.Timer(async (_) =>
{
Time = DateTime.Now.ToString();
databaseValue = await TimerProcessGetValue();
await InvokeAsync(StateHasChanged);
}, null, 0, 1000);
Change to verification code
_timer = new System.Threading.Timer(async (_) =>
{
Time = DateTime.Now.ToString();
//databaseValue = await TimerProcessGetValue();
await Task.Delay(500); //Added verification code.
await InvokeAsync(StateHasChanged);
}, null, 0, 1000);
}
Append.(10/10/2021)
Version used: net core 5.0.7.
Browser used: Edge.
I'm not using any other browser.
My environment is limited and I can only use Edge ...
Below are the three codes we have verified.
①This is an example using Dispose (WaitHandle).
This did not improve the error.
public void Dispose()
{
using(System.Threading.WaitHandle waitHandle = new System.Threading.ManualResetEvent(false))
{
if (_timer.Dispose(waitHandle))
{
const int millisecondsTimeout = 500;
if (!waitHandle.WaitOne(millisecondsTimeout))
{
System.Diagnostics.Debug.WriteLine("Dispose Test");
}
}
}
}
②This is an example using System.Timers.Timer.
the reload using the F5 key was successful.
private int currentCount = 0;
private Timer timer2 = new(1000);
protected override void OnInitialized()
{
timer2.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer2.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
currentCount++;
Time = DateTime.Now.ToString();
databaseValue = TimerProcessGetValue().Result;
StateHasChanged();
});
}
public void Dispose() => timer2.Dispose();
③Added error handling.
Even with this method, I succeeded in reloading with the F5 key without any error.
try
{
_timer = new System.Threading.Timer(async (_) =>
{
Time = DateTime.Now.ToString();
databaseValue = await TimerProcessGetValue();
await InvokeAsync(StateHasChanged);
}, null, 0, 1000);
}
catch
{
// empty on purpose
}
https://learn.microsoft.com/en-us/dotnet/api/system.threading.timer?view=net-5.0
When a timer is no longer needed, use the Dispose method to free the resources held by the timer. Note that callbacks can occur after the Dispose() method overload has been called, because the timer queues callbacks for execution by thread pool threads. You can use the Dispose(WaitHandle) method overload to wait until all callbacks have completed.
This is why await InvokeAsync(StateHasChanged); is being called after the component is disposed.
To expand on #Brianparker's answer:
replace #implements IDisposable with #implements IAsyncDisposable
and then replace the Dispose() method with
public ValueTask DisposeAsync()
{
return _timer?.DisposeAsync() ?? ValueTask.CompletedTask;
}
Update
but I get the same error
I can't reproduce your error but I suppose that F5 is rather brutal.
You could try putting a try/catch aroud the StateHasChanged call:
try
{
await InvokeAsync(StateHasChanged);
}
catch
{ // empty on purpose
}

Blazor Server Problem that page update by Timer stops

I have written a program that updates pages regularly with the Blazor server app.
When this program is executed, the current time and the value from the database table are acquired every second and displayed on the page, but the processing in this timer stops after a certain amount of time.
Although it is not measured accurately, the processing in the timer stops after about 10 to 30 minutes.
However, when I look at the debug tool of the browser, there is no error, and there is no error on visual studio.
I searched for an example like this but couldn't find it and couldn't determine what was causing it.
I want the processing in this timer to work until I explicitly specify it as stopped.
What could be the cause of this?
I'm in trouble because I don't know the cause.
Please give me some advice.
Thank you.
private string Time { get; set; }
protected override void OnInitialized()
{
var timer = new System.Threading.Timer((_) =>
{
Time = DateTime.Now.ToString();
InvokeAsync(() =>
{
// Get the value from the database
databaseValue = TimerProcessGetValue();
StateHasChanged();
});
}, null, 0, 1000);
base.OnInitialized();
}
Process called in Timer
public async Task<int?> TimerProcessGetValue()
{
int? timerProcessValue;
using(DbContext = DbFactory.CreateDbContext())
{
timerProcessValue = dbContext.TestTable.Single(x => x.Id == 1).TestValue;
}
return timerProcessValue;
}
postscript
The full text of the target component.
PageRefleshTest.cs
#page "/refresh-ui-manually"
#using System.Threading;
#using TestBlazorServer.Models;
#using Microsoft.EntityFrameworkCore;
#inject IDbContextFactory<SQLbeginnerContext> DbFactory
<h1>#Time</h1>
<h1>TestValue:#databaseValue</h1>
private string Time { get; set; }
private int? databaseValue { get; set; }
protected override void OnInitialized()
{
var timer = new System.Threading.Timer((_) =>
{
Time = DateTime.Now.ToString();
InvokeAsync(() =>
{
// Get the value from the database
databaseValue = TimerProcessGetValue().Result;
StateHasChanged();
});
}, null, 0, 1000);
base.OnInitialized();
}
public async Task<int?> TimerProcessGetValue()
{
int? timerProcessValue;
using (var dbContext = DbFactory.CreateDbContext())
{
timerProcessValue = dbContext.TestTable.Single(x => x.Id == 1).TestValue;
}
return timerProcessValue;
}
Implemented using System.Timers.Timer
#page "/Refresh2"
#inject IDbContextFactory<SQLbeginnerContext> DbFactory
#using TestBlazorServer.Models
#using Microsoft.EntityFrameworkCore
#using System.Timers
#implements IDisposable
<h3>PageRefresh2</h3>
<h1>#Time</h1>
<h1>#currentCount</h1>
<h1>TestValue:#databaseValue/h1>
#code {
private int currentCount = 0;
private Timer timer2 = new(1000);
private string Time { get; set; }
private int? databaseValue { get; set; }
protected override void OnInitialized()
{
timer2.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer2.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
currentCount++;
Time = DateTime.Now.ToString();
databaseValue = TimerProcessGetValue().Result;
StateHasChanged();
});
}
public void Dispose() => timer2.Dispose();
public async Task<int?> TimerProcessGetValue()
{
int? timerProcessValue;
await using (var dbContext = DbFactory.CreateDbContext())
{
timerProcessValue = dbContext.TestTable.Single(x => x.Id == 1).TestValue;
}
return timerProcessValue;
}
}
It could of course be from the SignalR connection failing. That should reconnect however.
The main suspect here is .Result. You should always avoid .Wait() and .Result in async code. Aslo, your DB method is not really async, that might play a role too.
But that 'half hour' could also come from the Garbage collector throwing out the Timer object. See the purple Note on this page. When you have a Timer you should anchor it in a field and Dispose() it.
The next step is to make the eventhandler an async void, this is one of the very few cases that is called for. And then only Invoke the StatehasChanged call.
#implements IDisposable
...
#code {
System.Threading.Timer _timer; // use a private field
protected override void OnInitialized()
{
_timer = new System.Threading.Timer(async (_) =>
{
Time = DateTime.Now.ToString();
databaseValue = await TimerProcessGetValue();
await InvokeAsync(StateHasChanged);
}, null, 0, 1000);
// base.OnInitialized(); -- not needed
}
public void Dispose()
{
_timer?.Dispose();
}
}
And make the Db action actually async:
public async Task<int?> TimerProcessGetValue()
{
int? timerProcessValue;
using (var dbContext = DbFactory.CreateDbContext())
{
timerProcessValue =
(await dbContext.TestTable.SingleAsync(x => x.Id == 1))
.TestValue;
}
return timerProcessValue;
}
Only you can test this in-situ, let us know if it works.

Trigger Async Function on Other Pages by Calling it in Non-Async Constructor in C#

In my App, there is an Async Function ProcessOffer(). When I called it in Constructor as ProcessOffer, it works but synchronously. I want to call this Function in constructor asynchronously.
The ProcessOffer() is a function implemented in CredentialViewModel, but I want that, It should be triggered asynchronously everywhere on the App (IndexViewModel e.t.c).
If I'm on the IndexPage, and Web Application sends a request to Mobile Application, ProcessOffer(), should be triggered.. actually what ProcessOffer does is, that it asks the user to enter a PIN, if it's correct, it sends back a response to the Web Application.
I've tried answers from other Posts, but they returned the Error Autofac.Core.DependencyResolutionException: 'An exception was thrown while activating App.Name, when I sends a request to Mobile App from Web Application.
The Solutions I tried.
1- https://stackoverflow.com/a/64012442/14139029
2- Task.Run(() => ProcessOffer()).Wait();
3- ProcessOffer().GetAwatier().GetResult();
CredentialViewModel.cs
namespace Osma.Mobile.App.ViewModels.Credentials
{
public class CredentialViewModel : ABaseViewModel
{
private readonly CredentialRecord _credential;
private readonly ICredentialService _credentialService;
private readonly IAgentProvider _agentContextProvider;
private readonly IConnectionService _connectionService;
private readonly IMessageService _messageService;
private readonly IPoolConfigurator _poolConfigurator;
[Obsolete]
public CredentialViewModel(
IUserDialogs userDialogs,
INavigationService navigationService,
ICredentialService credentialService,
IAgentProvider agentContextProvider,
IConnectionService connectionService,
IMessageService messageService,
IPoolConfigurator poolConfigurator,
CredentialRecord credential
) : base(
nameof(CredentialViewModel),
userDialogs,
navigationService
)
{
_credential = credential;
_credentialService = credentialService;
_agentContextProvider = agentContextProvider;
_connectionService = connectionService;
_messageService = messageService;
_poolConfigurator = poolConfigurator;
_credentialState = _credential.State.ToString();
if (_credentialState == "Offered")
{
ProcessOffer();
}
}
[Obsolete]
public async Task ProcessOffer()
{
foreach (var item in _credential.CredentialAttributesValues)
{
await SecureStorage.SetAsync(item.Name.ToString(), item.Value.ToString());
}
var RegisteredPIN = await SecureStorage.GetAsync("RegisteredPIN");
string PIN = await App.Current.MainPage.DisplayPromptAsync("Enter PIN", null, "Ok", "Cancel", null, 6, Keyboard.Numeric);
if (PIN == RegisteredPIN)
{
try
{
//await _poolConfigurator.ConfigurePoolsAsync();
var agentContext = await _agentContextProvider.GetContextAsync();
var credentialRecord = await _credentialService.GetAsync(agentContext, _credential.Id);
var connectionId = credentialRecord.ConnectionId;
var connectionRecord = await _connectionService.GetAsync(agentContext, connectionId);
(var request, _) = await _credentialService.CreateRequestAsync(agentContext, _credential.Id);
await _messageService.SendAsync(agentContext.Wallet, request, connectionRecord);
await DialogService.AlertAsync("Request has been sent to the issuer.", "Success", "Ok");
}
catch (Exception e)
{
await DialogService.AlertAsync(e.Message, "Error", "Ok");
}
}
else if (PIN != RegisteredPIN && PIN != null)
{
DialogService.Alert("Provided PIN is not correct");
}
}
#region Bindable Command
[Obsolete]
public ICommand ProcessOfferCommand => new Command(async () => await ProcessOffer());
public ICommand NavigateBackCommand => new Command(async () =>
{
await NavigationService.PopModalAsync();
});
#endregion
#region Bindable Properties
private string _credentialState;
public string CredentialState
{
get => _credentialState;
set => this.RaiseAndSetIfChanged(ref _credentialState, value);
}
#endregion
}
}
IndexViewModel.cs
namespace Osma.Mobile.App.ViewModels.Index
{
public class IndexViewModel : ABaseViewModel
{
private readonly IConnectionService _connectionService;
private readonly IMessageService _messageService;
private readonly IAgentProvider _agentContextProvider;
private readonly IEventAggregator _eventAggregator;
private readonly ILifetimeScope _scope;
public IndexViewModel(
IUserDialogs userDialogs,
INavigationService navigationService,
IConnectionService connectionService,
IMessageService messageService,
IAgentProvider agentContextProvider,
IEventAggregator eventAggregator,
ILifetimeScope scope
) : base(
"Index",
userDialogs,
navigationService
)
{
_connectionService = connectionService;
_messageService = messageService;
_agentContextProvider = agentContextProvider;
_eventAggregator = eventAggregator;
_scope = scope;
}
public override async Task InitializeAsync(object navigationData)
{
await base.InitializeAsync(navigationData);
}
public class Post
{
public string Success { get; set; }
public string firstname { get; set; }
}
[Obsolete]
public async Task ScanVerification(object sender, EventArgs e)
{
// Code
}
public async Task SettingsPage(SettingsViewModel settings) => await NavigationService.NavigateToAsync(settings, null, NavigationType.Modal);
#region Bindable Command
public ICommand SettingsPageCommand => new Command<SettingsViewModel>(async (settings) =>
{
await SettingsPage(settings);
});
[Obsolete]
public ICommand ScanVerificationCommand => new Command(async () => await ScanVerification(default, default));
#endregion
}
}
App.xml.cs
[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
namespace Osma.Mobile.App
{
public partial class App : Application
{
public new static App Current => Application.Current as App;
public static IContainer Container { get; set; }
// Timer to check new messages in the configured mediator agent every 10sec
private readonly Timer timer;
private static IHost Host { get; set; }
public App()
{
InitializeComponent();
timer = new Timer
{
Enabled = false,
AutoReset = true,
Interval = TimeSpan.FromSeconds(10).TotalMilliseconds
};
timer.Elapsed += Timer_Elapsed;
}
public App(IHost host) : this() => Host = host;
public static IHostBuilder BuildHost(Assembly platformSpecific = null) =>
XamarinHost.CreateDefaultBuilder<App>()
.ConfigureServices((_, services) =>
{
services.AddAriesFramework(builder => builder.RegisterEdgeAgent(
options: options =>
{
options.AgentName = "Mobile Holder";
options.EndpointUri = "http://11.222.333.44:5000";
options.WalletConfiguration.StorageConfiguration =
new WalletConfiguration.WalletStorageConfiguration
{
Path = Path.Combine(
path1: FileSystem.AppDataDirectory,
path2: ".indy_client",
path3: "wallets")
};
options.WalletConfiguration.Id = "MobileWallet";
options.WalletCredentials.Key = "SecretWalletKey";
options.RevocationRegistryDirectory = Path.Combine(
path1: FileSystem.AppDataDirectory,
path2: ".indy_client",
path3: "tails");
// Available network configurations (see PoolConfigurator.cs):
options.PoolName = "sovrin-test";
},
delayProvisioning: true));
services.AddSingleton<IPoolConfigurator, PoolConfigurator>();
var containerBuilder = new ContainerBuilder();
containerBuilder.RegisterAssemblyModules(typeof(CoreModule).Assembly);
if (platformSpecific != null)
{
containerBuilder.RegisterAssemblyModules(platformSpecific);
}
containerBuilder.Populate(services);
Container = containerBuilder.Build();
});
protected override async void OnStart()
{
await Host.StartAsync();
// View models and pages mappings
var _navigationService = Container.Resolve<INavigationService>();
_navigationService.AddPageViewModelBinding<MainViewModel, MainPage>();
_navigationService.AddPageViewModelBinding<RegisterViewModel, RegisterPage>();
_navigationService.AddPageViewModelBinding<IndexViewModel, IndexPage>();
_navigationService.AddPageViewModelBinding<SettingsViewModel, SettingsPage>();
_navigationService.AddPageViewModelBinding<CredentialsViewModel, CredentialsPage>();
_navigationService.AddPageViewModelBinding<CredentialViewModel, CredentialPage>();
if (Preferences.Get(AppConstant.LocalWalletProvisioned, false))
{
await _navigationService.NavigateToAsync<MainViewModel>();
}
else
{
await _navigationService.NavigateToAsync<ProviderViewModel>();
}
timer.Enabled = true;
}
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
// Check for new messages with the mediator agent if successfully provisioned
if (Preferences.Get(AppConstant.LocalWalletProvisioned, false))
{
Device.BeginInvokeOnMainThread(async () =>
{
try
{
var context = await Container.Resolve<IAgentProvider>().GetContextAsync();
await Container.Resolve<IEdgeClientService>().FetchInboxAsync(context);
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
});
}
}
protected override void OnSleep() =>
// Stop timer when application goes to background
timer.Enabled = false;
protected override void OnResume() =>
// Resume timer when application comes in foreground
timer.Enabled = true;
}
}
Look, you have public override async Task InitializeAsync(object navigationData) method in the IndexViewModel class. I suppose the framework invoke it to initialize the view model. So, thus CredentialViewModel inherits same ABaseViewModel anyway, why wouldn't you just override InitializeAsync in your CredentialViewModel and call ProcessOffer from it?
public CredentialViewModel(
IUserDialogs userDialogs,
INavigationService navigationService,
ICredentialService credentialService,
IAgentProvider agentContextProvider,
IConnectionService connectionService,
IMessageService messageService,
IPoolConfigurator poolConfigurator,
CredentialRecord credential
) : base(
nameof(CredentialViewModel),
userDialogs,
navigationService
)
{
_credential = credential;
_credentialService = credentialService;
_agentContextProvider = agentContextProvider;
_connectionService = connectionService;
_messageService = messageService;
_poolConfigurator = poolConfigurator;
_credentialState = _credential.State.ToString();
}
public override async Task InitializeAsync(object navigationData)
{
if (_credentialState != "Offered") return;
await ProcessOffer();
}
Anyway, you have to avoid calling asynchronous operations in constructors.
So in my View Models, if i need to initialize asynchronously I usually pass OnAppearing() to the view model instead of the constructor. Someone can tell me if this is unwise but it solved my needs.
Code Behind View:
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class CredentialView : ContentPage
{
public CredentialView()
{
InitializeComponent();
BindingContext = new CredentialViewModel();
}
protected override void OnAppearing()
{
base.OnAppearing();
((CredentialViewModel)BindingContext).OnAppearing();
}
}
View Model:
public class CredentialViewModel : INotifyPropertyChanged
{
public CredentialViewModel ()
{
}
public async void OnAppearing()
{
await ProcessOffer();
}
}
Also just FYI, the biggest gotcha i've experienced with asynchronous code on xamarin is to use BeginInvokeOnMainThread. It's my understanding that touched UI should be executed on main thread! (probably why they call it the UI thread. haha)
Example:
Device.BeginInvokeOnMainThread(() =>
{
observableCollection.Clear();
});
As you've already asked before, kicking off async code in the constructor of a class can be a can of worms. You should instead consider doing either:
Starting ProcessOffer in a lifecycle method instead. For instance when the View is showing call ProcessOfferCommand.
Using fire and forget to not block the constructor: Task.Run(ProcessOffer) you should probably avoid this though.
Use something like NotifyTask Stephen Cleary describes here: https://learn.microsoft.com/en-us/archive/msdn-magazine/2014/march/async-programming-patterns-for-asynchronous-mvvm-applications-data-binding you can find the complete code for it here: https://github.com/StephenCleary/Mvvm.Async/blob/master/src/Nito.Mvvm.Async/NotifyTask.cs
Use a CreateAsync pattern with a private constructor. However, this doesn't work well with Dependency Injection usually. Doing IO intensive work during resolution isn't really the correct place to do it.
In my opinion using either 1. or 3. would be the best solutions, perhaps leaning towards a combination of 1. using NotifyTask that can notify you when it is done loading.

What prevents this task from being long running?

For testing purposes, I am using this directly inside of a razor block in a .cshtml page.
#functions{
public class Inline
{
public HttpResponseBase r { get; set; }
public int Id { get; set; }
public List<System.Threading.Tasks.Task> tasks = new List<System.Threading.Tasks.Task>();
public void Writer(HttpResponseBase response)
{
this.r = response;
tasks.Add(System.Threading.Tasks.Task.Factory.StartNew(
() =>
{
while (true)
{
r.Write("<span>Hello</span>");
System.Threading.Thread.Sleep(1000);
}
}
));
}
}
}
#{
var inL = new Inline();
inL.Writer(Response);
}
I had expected it to write a span with the text "Hello" once every second. It will write "Hello" once sometimes, but not every time or even most times. Why isn't this task long running?
The reason you are seeing different result is because the task is running asynchronously and if the response object is completed before your task gets a chance to write on it, the taks will throw exception and it will terminate the only way you can do this is if you add Task.WaitAll() at the end of the Writer() method.
This will work but the page will not stop loading content.
this.r = response;
tasks.Add(System.Threading.Tasks.Task.Factory.StartNew(
() =>
{
while (true)
{
r.Write("<span>Hello</span>");
r.Flush(); // this will send each write to the browser
System.Threading.Thread.Sleep(1000);
}
}
));
//this will make sure that the response will stay open
System.Threading.Tasks.Task.WaitAll(tasks.ToArray());
Here is another option this one uses a custom ActionResult , it first process the controller (the default result) after that is done it starts the task.
public class CustomActionResult:ViewResult
{
public override void ExecuteResult(ControllerContext context)
{
base.ExecuteResult(context);
var t = Task.Factory.StartNew(() =>
{
while (true)
{
Thread.Sleep(1000);
context.HttpContext.Response.Write("<h1>hello</h1>");
context.HttpContext.Response.Flush();
}
});
Task.WaitAll(t);
}
}
In your controller
public class HomeController : Controller
{
public ActionResult Index()
{
return new CustomActionResult();
}
}

Categories

Resources