ObservableCollection AddRange in async Task - wpf

Hi I want to use the ObservableCollection (AddRange) in async Task but i get NotSupportedException
private ObservableCollection<CoronavirusCountry> _data = new ObservableCollection<CoronavirusCountry>();
public ObservableCollection<CoronavirusCountry> data
{
get => _data;
set => SetProperty(ref _data, value);
}
Task.Run(async()=>{
APIService service = new APIService();
data.AddRange(await service.GetTopCases());
Status = "Updated " + DateTime.Now;
});

Not sure which AddRange method you are referring to, because ObservableCollection doesn't have that out of the box.
Anyway - assuming you wrote an extension method - it has to be called in the UI thread, so running a Task doesn't make sense.
The awaitable method shown below should be sufficient. It would await the asynchronous service call, and update the collection in the main thread.
public async Task UpdateData()
{
var service = new APIService();
var newData = await service.GetTopCases();
Data.AddRange(newData); // use proper naming!
Status = "Updated " + DateTime.Now;
}
In order to call and await the above method, you could have an async Loaded event handler like this:
public MainWindow()
{
InitializeComponent();
viewModel = new ViewModel();
Loaded += async (s, e) => await viewModel.UpdateData();
}

Related

Datagrid remains empty after asynchronous initialization in view model constructor

I have a WPF application with a view containing a data grid and a view model with an observable collection that is initialized by calling an asynchronous method in the constructor. But the data grid remains empty upon running the code.
The view model class looks like this.
internal class MainWindowViewModel : INotifyPropertyChanged
{
private readonly IBookingRecordService service;
public event PropertyChangedEventHandler? PropertyChanged;
private ObservableCollection<BookingRecord> bookingRecords = new();
public ObservableCollection<BookingRecord> BookingRecords
{
get => bookingRecords;
set
{
bookingRecords = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BookingRecords)));
}
}
public MainWindowViewModel()
{
service = new BookingRecordService();
Task.Run(() => LoadBookingRecords());
}
private async Task LoadBookingRecords()
{
BookingRecords = new ObservableCollection<BookingRecord>(await service.Get());
}
}
I all LoadBookingRecords() in the constructor, so that the data starts loading on initialization of the view model already but I do it asynchronously, so it does not block the UI thread and makes the application unresponsive.
I have tried waiting for the completion of the task in the constructor via
Task.Run(() => LoadBookingRecords()).Wait();
to check that this has something to do with the asynchronous function call. And indeed, if I wait for the method to finish in the constructor, the data grid displays correctly. But I don't want to wait for the task to finish on the UI thread because it blocks the UI.
I have read that you must raise the PropertyChanged event on the UI thread to trigger a UI update and I suppose that is the problem here. I have also read that one can use
Application.Current.Dispatcher.BeginInvoke()
to schedule a delegate to run on the UI thread as soon as possible, so I tried the following.
private async Task LoadBookingRecords()
{
await Application.Current.Dispatcher.BeginInvoke(new Action(async () =>
{
BookingRecords = new ObservableCollection<BookingRecord>(await service.Get());
}));
}
But this leaves the DataGrid empty as well.
"'asynchronous ... in constructor" is something you must avoid.
Async calls must be awaited, which can not be done in a constructor. Declare an awaitable Initialize method instead
public Task Initialize()
{
return LoadBookingRecords();
}
and call it in an async Loaded event handler in your MainWindow:
private static async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
await viewModel.Initialize();
}
Alternatively, create a factory method like
public static async Task<MainWindowViewModel> Create()
{
var viewModel = new MainWindowViewModel();
await viewModel.LoadBookingRecords();
return viewModel;
}
and call that in the Loaded handler:
private static async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
DataContext = await MainWindowViewModel.Create();
}
Building on Clemens' answer, I tried something a little different in order to avoid touching the MainWindow code-behind.
I removed the call on LoadBookingRecords in the constructor and instead created a delegate command as a property that holds this method.
internal class MainWindowViewModel : INotifyPropertyChanged
{
private readonly IBookingRecordService service;
private ObservableCollection<BookingRecord> bookingRecords = new();
public ICommand LoadBookingRecordsCommand { get; set; }
public ObservableCollection<BookingRecord> BookingRecords
{
get => bookingRecords;
set
{
bookingRecords = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BookingRecords)));
}
}
public MainWindowViewModel()
{
service = new BookingRecordService();
LoadBookingRecordsCommand = new DelegateCommand(async _ => await LoadBookingRecords());
}
private async Task LoadBookingRecords()
{
BookingRecords = new ObservableCollection<BookingRecord>(await service.Get());
}
}
I then added the NuGet package Microsoft.Xaml.Behaviors.Wpf to the project and added the following namespace to the MainWindow XAML.
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
Finally, I bound the delegate command to the MainWindow's Loaded event.
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding LoadBookingRecordsCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
Now the data grid displays correctly after being loaded.

OnAfterRender or OnInitializedAsync function to refresh data?

I'd like to refresh my data each minute. for this, I use a timer.
`
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
//Configuration des graphiques
Alert.Info("OnInitializedAsync");
timer = new System.Threading.Timer(async (object? stateInfo) =>
{
loading = true;
GetDataAPI();
}, new System.Threading.AutoResetEvent(false), 2000, 2000);
}
`
this work fine, but when I load the page for the fisrt time, It spend a long time before to load data. when I delete the Time it's very faster.
so my question, is it in the OnInitializedAsync that I use the timer ? I've read a lot of documention on the cycle but don't really see the difference between OnAfterRender or OnInitializedAsync.
should I load data the first time in OnAfterRender with FirstRender ? and then the timer in OnInitializedAsync ?
thanks for your help.
You can break out the timer into a separate class with an event to drive updates:
public class MyTimer
{
private System.Timers.Timer _timer = new System.Timers.Timer();
public event EventHandler<ElapsedEventArgs>? TimerElapsed;
public MyTimer(double period)
=> SetTimer(period);
private void SetTimer(double period)
{
_timer = new System.Timers.Timer(period);
_timer.Elapsed += OnTimedEvent;
_timer.AutoReset = true;
_timer.Enabled = true;
}
private void OnTimedEvent(object? source, ElapsedEventArgs e)
=> this.TimerElapsed?.Invoke(this, e);
}
And then you can use it like this. I've added a simple message that is updated every 5 seconds to demo getting new data. Note there's no delay on the initial load.
#page "/"
#implements IDisposable
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<div class="alert alert-info">
#message
</div>
#code {
private MyTimer? timer;
private string message = DateTime.Now.ToLongTimeString();
private bool isGettingData;
protected async override Task OnInitializedAsync()
{
await this.GetDataAsync();
// Set for every 5 seconds
timer = new(5000);
timer.TimerElapsed += this.OnTimeElapsed;
}
private async void OnTimeElapsed(object? sender, EventArgs e)
{
await this.GetDataAsync();
// Update the UI
await this.InvokeAsync(StateHasChanged);
}
private async ValueTask GetDataAsync()
{
// Only get the data again if we finished the last get
if (isGettingData)
return;
isGettingData = true;
// emulate async fetching data
await Task.Delay(100);
message = DateTime.Now.ToLongTimeString();
isGettingData = false;
}
public void Dispose()
{
if (timer is not null)
timer.TimerElapsed -= this.OnTimeElapsed;
timer = null;
}
}

Prism, IConfirmNavigationRequest, InteractionRequest and Async

I have an issue, i coded my view, viewmodel creation into the ModuleInit.Initialize method
this.container.RegisterType<IControlPanel, ViewModels.SeveritiesViewModel>("SeveritiesViewModel");
this.container.RegisterType<object, Views.SeveritiesView>("SeveritiesView", new InjectionConstructor(new ResolvedParameter<IControlPanel>("SeveritiesViewModel")));
SeveritiesVeiwModel inherits from ViewModelBase
public class ViewModelBase : BindableBase, IControlPanel, INavigationAware, IConfirmNavigationRequest
Constructor for ViewModelBase calls two virtual methods. Initialize and GetData.
GetData performs some data access methods using async await.
so the problem i have is Prism constructs my SeveritiesViewModel, the GetData method runs, and throws and exception which i catch. i would then like to display a dialog using the InteractionRequest, however the view.DataContext has not yet be set, hence no bindings or Interaction.Triggers to receive the InteractionRequest.
so i thought i should look into RegionManager.RequestNaviagte using a callback. i thought since all my viewmodels implement IConfirmNavigationRequest i could return false in the NavigationResult from the View/viewmodel being injected. however ConfirmNavigationRequest is never called. this is wpf not silverlight?
so how do i work this extremely decoupled application. do i need to implement some type of shared service?
I guess i am going to need to store exceptions until the view has finished binding with the viewmodel, perhaps implement my own interface with a method to check an exceptions collection and in the view call the interface method?
why is ConfirmNavigationRequest never called?
InteractionRequest work great after the DataContext is set, but before; i'm at a loss.
Any advise would be appreciated.
Thanks
Gary
here is some code.
toolbar button command click runs the following.
this.regionManager.RequestNavigate("ContentRegion", "SeveritiesView");
here is the code behind for the view.
public partial class SeveritiesView : UserControl, IApplicationView
{
public SeveritiesView(IControlPanel model)
{
InitializeComponent();
this.DataContext = model;
}
public string ViewName
{
get { return "SeveritiesView"; }
}
}
ViewModelBase.
protected ViewModelBase(bool initializeDB = true)
{
notifications = new List<NotificationWindowNotification>();
this.uiFactory = new TaskFactory(TaskScheduler.FromCurrentSynchronizationContext());
NotificationRequest = new InteractionRequest<NotificationWindowNotification>();
ConfirmationRequest = new InteractionRequest<ConfirmationWindowNotification>();
if (initializeDB)
{
EntityConnectionStringBuilder entityBuilder = new EntityConnectionStringBuilder(System.Configuration.ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString);
entityBuilder.ProviderConnectionString = EventLogAnalysis.Properties.Settings.Default.ConnectionString;
db = new ServerEventLogEntities(entityBuilder.ToString());
}
ThrobberVisible = Visibility.Visible;
Initialize();
GetData();
}
SeveritiesViewModel.
public SeveritiesViewModel(IRegionManager regionManager, IEventAggregator eventAggregator) : base()
{
try
{
this.regionManager = regionManager;
this.eventAggregator = eventAggregator;
eventAggregator.GetEvent<AddSeverity>().Subscribe(AddSeverity);
eventAggregator.GetEvent<DeleteSeverity>().Subscribe(DeleteSeverity);
}
catch(Exception e)
{
uiFactory.StartNew(() =>
NotificationRequest.Raise(new NotificationWindowNotification()
{
Title = string.Format("Error during {0}.{1}"
, ModuleName, System.Reflection.MethodBase.GetCurrentMethod().Name),
Content = string.Format("{0}", e.Message)
})
).Wait();
}
}
protected async override void GetData()
{
try
{
List<Task> tasks = new List<Task>();
tasks.Add(GetEventFilterSeverities());
await Task.WhenAll(tasks).ContinueWith((t) =>
{
ThrobberVisible = Visibility.Collapsed;
eventAggregator.GetEvent<RecordStatusEvent>().Publish(new RecordStatusMessage() { CanAdd = true, CanDelete =(currentEventFilterSeverity != null), IsClosing = false });
}
, TaskScheduler.FromCurrentSynchronizationContext());
}
catch(Exception e)
{
notifications.Add(new NotificationWindowNotification()
{
Title = string.Format("Error during {0}.{1}"
, ModuleName, System.Reflection.MethodBase.GetCurrentMethod().Name),
Content = string.Format("{0}", e.Message)
});
}
}
protected async Task GetEventFilterSeverities()
{
try
{
throw new NullReferenceException("My exception");
ObservableCollection<EventFilterSeverity> _eventFilterSeverities = new ObservableCollection<EventFilterSeverity>();
var eventFilterSeverities = await (from sg in db.EventFilterSeverities
orderby sg.EventFilterSeverityID
select sg).ToListAsync();
foreach (EventFilterSeverity efs in eventFilterSeverities)
_eventFilterSeverities.Add(efs);
EventFilterSeverities = _eventFilterSeverities;
}
catch(Exception e)
{
notifications.Add(new NotificationWindowNotification()
{
Title = string.Format("Error during {0}.{1}"
, ModuleName, System.Reflection.MethodBase.GetCurrentMethod().Name),
Content = string.Format("{0}", e.Message)
});
}
}
Two fairly easy solutions;
Do not start data access until the Shell has been displayed and interactions are possible
Catch the exception and immediatly await on a Task that completes when the interaction request becomes available. Is this when the navigation completes? This effectively queues the interaction for when it can be displayed.
This looks promising.
in the view
<i:Interaction.Triggers>
<i:EventTrigger EventName="Raised" SourceObject="{Binding NotificationRequest}">
<i:EventTrigger.Actions>
<dialogs:PopupWindowAction IsModal="True"/>
</i:EventTrigger.Actions>
</i:EventTrigger>
<i:EventTrigger EventName="Loaded">
<ei:CallMethodAction TargetObject="{Binding Mode=OneWay}" MethodName="DisplayPreBoundExceptions"/>
</i:EventTrigger>
</i:Interaction.Triggers>
In the ViewModelBase
public void DisplayPreBoundExceptions()
{
notifications.ForEach((t) => NotificationRequest.Raise(t));
notifications.Clear();
}

Relay command using injected data service not invoked

When I use an injected instance of a data service in the lambda statement that defines a relay command handler, the handler is never invoked (it is associated with a button). When I declare an instance of the data service within the lambda, it works fine. Any ideas?
Edited:
Created the class variable _dataService and initialized it in the view model constructor. Use the class variable within the relay command handler and all works.
private IDataService _dataService;
public MainViewModel(IDataService dataService)
{
_dataService = dataService;
Batches = new ObservableCollection<Batch>();
#region RefreshCommand
RefreshCommand = new RelayCommand(
() =>
{
var t1 = Task<ObservableCollection<Batch>>.Factory.StartNew(() =>
{
// WHEN I UNCOMMENT AND COMMENT OUT BELOW, WORKS FINE.
//DataService test = new DataService();
//ObservableCollection<Batch> batches = test.GetBatchesToProcess();
//
// THIS NOW WORKS.
return _dataService.GetBatchesToProcess();
});
try
{
t1.Wait();
}
catch (AggregateException ae)
{
foreach (var e in ae.InnerExceptions)
{
if (e is SqlException)
{
MessageBox.Show("Sql Exception: " + e.Message);
}
else
{
MessageBox.Show("Unexpected error: " + e.Message);
}
}
return;
}
Batches = t1.Result;
}
);
#endregion
}
Using the dataService parameter to the MainViewModel constructor did not work within the relay command handler. Using a private class variable (_dataService) that is initialized within the constructor solved the dilemma.

How to verify the EventAggregator's unsubscribe method is called when disposing a ViewModel with Prism

I'm struggling to write a test that confirms that I am correctly unsubscribing from an EventAggregator's message when it is closed. Anyone able to point out the (simple) answer?!
Here is the code:
public class ViewModel : BaseViewModel, IViewModel
{
private readonly IEventAggregator eventAggregator;
private SubscriptionToken token;
IssuerSelectedEvent issuerSelectedEvent;
public ViewModel(IView view, IEventAggregator eventAggregator)
{
this.eventAggregator = eventAggregator;
View = view;
issuerSelectedEvent = eventAggregator.GetEvent<IssuerSelectedEvent>();
token = issuerSelectedEvent.Subscribe(SelectedIssuerChanged, true);
}
private void SelectedIssuerChanged(IssuerSelectedCommand obj)
{
Console.WriteLine(obj);
}
public IView View { get; set; }
public override void Dispose()
{
issuerSelectedEvent.Unsubscribe(token);
}
}
The test fails with:
Moq.MockVerificationException : The following setups were not matched:
IssuerSelectedEvent x => x.Unsubscribe(It.IsAny())
Here is the test:
[Test]
public void UnsubscribeFromEventAggregatorOnDispose()
{
var view = new Mock<ICdsView>();
var ea = new Mock<EventAggregator>();
var evnt = new Mock<IssuerSelectedEvent>();
evnt.Setup(x => x.Unsubscribe(It.IsAny<SubscriptionToken>()));
var vm = new CdsIssuerScreenViewModel(view.Object, ea.Object);
vm.Dispose();
evnt.VerifyAll();
}
Here I am verifying that the Unsubscribe was called on the mocked IssuerSelectedEvent
[Test]
public void UnsubscribeFromEventAggregatorOnDispose()
{
var view = new Mock<ICdsView>();
var ea = new Mock<IEventAggregator>();
var evnt = new Mock<IssuerSelectedEvent>();
ea.Setup(x => x.GetEvent<IssuerSelectedEvent>()).Returns(evnt.Object);
var vm = new CdsIssuerScreenViewModel(view.Object, ea.Object);
vm.Dispose();
evnt.Verify(x => x.Unsubscribe(It.IsAny<SubscriptionToken>());
}
If you want to check that the exact same token is passed into the Unsubscribe then you will need a Setup for the Subscribe method that returns a token you create in your test.
You need to tell your EventAggregator mock to return your mocked IssuerSelectedEvent:
ea.Setup(x => x.GetEvent<IssuerSelectedEvent>()).Return(evnt.Object);
The tests needs to be changed to:
[Test]
public void UnsubscribeFromEventAggregatorOnDispose()
{
var view = new Mock<ICdsView>();
var ea = new Mock<IEventAggregator>();
var evnt = new Mock<IssuerSelectedEvent>();
ea.Setup(x => x.GetEvent<IssuerSelectedEvent>()).Returns(evnt.Object);
evnt.Setup(x => x.Unsubscribe(It.IsAny<SubscriptionToken>()));
var vm = new CdsIssuerScreenViewModel(view.Object, ea.Object);
vm.Dispose();
evnt.VerifyAll();
}

Resources