I've been working on setting up a few WPF UserControls to load their external API data async, and it's mostly been going well- the pattern and base classes I made have worked for 99% of the use cases. However, one control and service, in particular, are giving me a hard time.
The external API service in the example is proven, tested, and working in other async parts of the application, so really the "new behavior" is calling the service from a non-async context, and using the dispatcher to shuttle the results back to the UI thread. Which to be more specific, this pattern is working, the data is being updated in the UI, but the CanExecute method is never being hit on update.
View Model
public ControlViewModel(IExternalApiService apiService)
{
_apiService = apiService;
}
public void Load(int idToLoad)
{
if (idToLoad == _lastIdLoaded) return;
Task.Run(async () => await InternalLoad(idToLoad));
}
public async Task InternalLoad(int id)
{
var apiResult = await _apiService.SpecificEndpoint.GetByIdAsync(id);
Dispatcher.CurrentDispatcher.Invoke(() =>
{
CurrentAPIResult = apiResult;
_lastIdLoaded = id;
}
}
private APIResult _currentAPIResult;
public APIResult CurrentAPIResult
{
get => _currentAPIResult;
set
{
Set(ref _currentAPIResult, value);
ExampleCommand.RaiseCanExecuteChanged();
}
}
private RelayCommand _exampleCommand;
public RelayCommand ExampleCommand => _exampleCommand ?? (_exampleCommand = new RelayCommand(RunCommand, CanRunCommand));
private bool CanRunCommand
{
return CurrentAPIResult != null;
}
public RunCommand()
{
//anotherExampleService.DoWork(CurrentAPIResult);
}
The issue is that the CanRunCommand
is never called after the CurrentAPIResult
is set, and I'm assuming that there is something more nuanced than I understand about calling the ExampleCommand.RaiseCanExecuteChanged()
.
If I set a breakpoint in the CurrentAPIResult
setter, I can step over that call, so I know it's being made, it's just as if it's still be called from the non-UI thread.
WPF .Net Framework 4.8, MVVMLight
I have tried various combinations of Dispatcher methods- BeginInvoke
, InvokeAsync
, etc... without success.
I've tried creating a separate method to call within the Dispatched call, but that also didn't work.
private void Update(APIResult currentAPIResult, int id)
{
CurrentAPIResult = currentAPIResult;
_lastIdLoaded = id;
}
So I hope I'm just overlooking something fairly simple here, and that someone can help me out.
Edit:
I found the existing examples less than complete, so I just wanted to edit the question with a working example, so that anyone else landing here can see a corrected implementation of this pattern.
This thread at some more information on the difference between the Dispatcher.CurrentDispatcher
and the Application.Current.Dispatcher
:
Dispatcher.CurrentDispatcher vs. Application.Current.Dispatcher
Working example:
View Model
public ControlViewModel(IExternalApiService apiService)
{
_apiService = apiService;
}
public void Load(int idToLoad)
{
if (idToLoad == _lastIdLoaded) return;
Task.Run(async () => await InternalLoad(idToLoad));
}
public async Task InternalLoad(int id)
{
var apiResult = await _apiService.SpecificEndpoint.GetByIdAsync(id);
Application.Current.Dispatcher.Invoke(() =>
{
CurrentAPIResult = apiResult;
_lastIdLoaded = id;
}
}
private APIResult _currentAPIResult;
public APIResult CurrentAPIResult
{
get => _currentAPIResult;
set
{
Set(ref _currentAPIResult, value);
ExampleCommand.RaiseCanExecuteChanged();
}
}
private RelayCommand _exampleCommand;
public RelayCommand ExampleCommand => _exampleCommand ?? (_exampleCommand = new RelayCommand(RunCommand, CanRunCommand));
private bool CanRunCommand
{
return CurrentAPIResult != null;
}
public RunCommand()
{
//anotherExampleService.DoWork(CurrentAPIResult);
}
Some things I found helpful:
The Application.Current.Dispatcher
is in the System.Windows
namespace.
I was able to set breakpoints in LoadInternal
and CurrentAPIResults
setter and open the Debug > Windows > Threads
view, to watch which thread the LoadInternal
and subsequent setter was being called from, to confirm where the execution was taking place.