Search code examples
wpfcaliburn.micro

ViewModel won't handle a EventAggregator event


I only seem to be able to handle EventAggregator events from the ShellViewModel, but I want to handle it from LoginViewModel.

The ShellViewModel constructs LoginViewModel as it's Active Item. I've also set it up to inherit from IHandle<AuthenticatedMessage> as a test that event publishing is working. It is able to handle that event. I haven't shown any Unsubscribe events in my code for brevity.

public class ShellViewModel : Conductor<IScreen>.Collection.OneActive, IHandle<AuthenticatedMessage>    
{
    public ShellViewModel(IEventAggregator eventAggregator, LoginViewModel loginViewModel)
    {
        _eventAggregator = eventAggregator;
        _loginViewModel = loginViewModel;
    }
    
    protected override async Task OnActivateAsync(CancellationToken cancellationToken)
    {
        await base.OnActivateAsync(cancellationToken);
        _eventAggregator.SubscribeOnPublishedThread(this);
    
        await ActivateItemAsync(_loginViewModel);
    }
    
    public async Task HandleAsync(AuthenticatedMessage authCode, CancellationToken cancellationToken)
    {
        // this is reached! So the event is publishing successfully.
        await Task.CompletedTask;
    }
}

LoginViewModel also subscribes to this event, but it's Handle method is not invoked.

The Login method is responsible for creating the LoginWindowViewModel Window (shown underneath) which publishes the event.

public class LoginViewModel : Screen, IHandle<AuthenticatedMessage>
{
    private IEventAggregator _eventAggregator;
    private readonly IWindowManager _windowManager;
    private readonly ILoginWindowViewModelFactory _LoginWindowViewModelFactory;

    public LoginViewModel(IEventAggregator eventAggregator, 
        IWindowManager windowManager, 
        ILoginWindowViewModelFactory loginWindowViewModelFactory)
    {
        _eventAggregator = eventAggregator; 
        _windowManager = windowManager;
        _LoginWindowViewModelFactory = loginWindowViewModelFactory;
    }
    
    protected override async Task OnActivateAsync(CancellationToken cancellationToken)
    {
        await base.OnActivateAsync(cancellationToken);
        
        _eventAggregator.SubscribeOnPublishedThread(this);
    }
    
    // This is bound to a button click event. It creates a window.
    public async void Login()
    {
        Uri loginUri = new Uri(_api.BaseLoginUrl);

        await _windowManager.ShowWindowAsync(
            _ndLoginWindowViewModelFactory.Create(loginUri, _eventAggregator));
    }       
    
    
    public async Task HandleAsync(AuthenticatedMessage authCode, CancellationToken cancellationToken)
    {
        // why is this is never reached?
        await Task.CompletedTask;
    }
}

The LoginWindowViewModel that publishes a AuthenticatedMessage event:

public class LoginWindowViewModel : Screen
{
    private readonly IEventAggregator _eventAggregator;
    private readonly Uri _initialUri;         

    public NDLoginWindowViewModel(Uri initialUri, IEventAggregator eventAggregator)
    {
        _initialUri = initialUri;
        _eventAggregator = eventAggregator;
    }       
    
    // bound to the WebView2 (browser control) event
    public async void NavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs e)
    {            
            string authCode = HttpUtility.ParseQueryString(new Uri(e.Uri).Query).Get("code");
            
            // Publish event here. LoginViewModel should handle this, but currently only ShellViewModel can.
            await _eventAggregator.PublishOnUIThreadAsync(new AuthenticatedMessage(authCode));                              
        }
    }
}

Solution

  • I resolved the issue after moving eventAggregator.SubscribeOnPublishedThread(this); to the LoginViewModel constructor, instead of the OnActivateAsync() method.

    From here:

    protected override async Task OnActivateAsync(CancellationToken cancellationToken)
    {
        await base.OnActivateAsync(cancellationToken);
        
        _eventAggregator.SubscribeOnPublishedThread(this);
    }
    

    To here:

    public LoginViewModel(IEventAggregator eventAggregator, 
        IWindowManager windowManager, 
        ILoginWindowViewModelFactory loginWindowViewModelFactory)
    {
        _eventAggregator = eventAggregator; 
        _windowManager = windowManager;
        _LoginWindowViewModelFactory = loginWindowViewModelFactory;
    
        _eventAggregator.SubscribeOnPublishedThread(this);
    }
    

    EDIT:

    The OnActivateAsync method isn't being called when the View is first created in the ShellViewModel because it is my root Screen and Conductor. So the Subscription was never taking place.

    public class ShellViewModel : Conductor<IScreen>.Collection.OneActive
    {
    ...
        protected override async Task OnActivateAsync(CancellationToken cancellationToken)
        {
            await base.OnActivateAsync(cancellationToken);            
        
            await ActivateItemAsync(_loginViewModel);
            // IsActive = false here, therefore the child Screen `_loginViewModel` 
            // is also not active. Result is that OnActivateAsync 
            // in this view model does not get called.
        }
    }
    

    It is directly related to this problem and answer.

    That explains why moving it to the constructor solved the problem.

    The final solution was to add _eventAggregator.SubscribeOnPublishedThread(this); in both the Constructor AND OnActivateAsync method. This allows me to resubscribe to the event after I navigate away from this viewmodel and come back to it.