0

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.

Andy Stagg
  • 373
  • 5
  • 22
  • You are using `Dispatcher.CurrentDispatcher` when invoking. That property is always only going to return the dispatcher for the current thread. See duplicate. Fact is, you shouldn't need to do anything special at all. WPF handles cross-thread invocation for data binding updates automatically. Just set the property from whatever thread you like. – Peter Duniho Jul 05 '21 at 20:38
  • Your RaiseCanExecuteChanged fires on a worker thread. UI Controls wont notice. To get the dispatcher of the UI-Thread `Application.Current.Dispatcher` is usually the straightest way a VM. Not `Dispatcher.CurrentDispatcher` as Pete pointed out. If your code is clean, you rarely have to use Dispatcher.Invoke/BeginInvoke at all even with async/await. – lidqy Jul 05 '21 at 21:27
  • @lidgy What exactly do you consider clean? I've been refactoring these controls out from a God VM/Control into highly specific VM/Controls where there is very little else in each control other than what you see here. Some are slightly more complex in the sense of that suppress data loads if they aren't expanded or on visible tabs. My understanding was that I am specifically creating a task (as I am in the example to run the async methods) that the dispatcher would need to be involved to update the UI thread. – Andy Stagg Jul 05 '21 at 21:41
  • Basically with 'clean' I mean, you don't run into cross-threading problems when you simply do the VM property assignments (which are bound to the View and cause these x-thread-issues) that you do that in top-level code, called by the UI. Because it's (almost ever) guaranteed to run on the Main thread (=UI thread). In your case I'd simply set `CurrentAPIResult` in 'Load', not in 'InternalLoad', presuming Load is called from the main thread. Of course you would need to make Load async aswell. If things are too complex you can still use Dispatcher.BeginInvoke, just choose the UI dispatcher .. – lidqy Jul 05 '21 at 22:05

0 Answers0