Caliburn-Micro / Caliburn.Micro

A small, yet powerful framework, designed for building applications across all XAML platforms. Its strong support for MV* patterns will enable you to build your solution quickly, without the need to sacrifice code quality or testability.
http://caliburnmicro.com/
MIT License
2.79k stars 776 forks source link

Question: Conductor<IScreen>.Collection.AllActive child view lifecycle methods #805

Open gerfen opened 2 years ago

gerfen commented 2 years ago

I am working on a WPF app and I have a view model which inherits from Conductor.Collection.AllActive. I add child views to the Items collection like so:

 await ActivateItemAsync(IoC.Get<ViewModel1>());
 await ActivateItemAsync(IoC.Get<ViewModel2>());

Both ViewModel1 and ViewModel2 inherit from Screen so I would expect the OnViewAttached, OnViewLoaded and OnViewReady methods to be fired but they do not. OnActivateAsync as well as OnInitalizeAsync are called as expected.

As an experiment, I wrote the following method on my Conductor implementation:

 protected async Task ActivateItemAsync<TViewModel>() where TViewModel : class, IScreen
 {
        var viewModel = IoC.Get<TViewModel>();
        var view = ViewLocator.LocateForModel(viewModel, null, null);
        ViewModelBinder.Bind(viewModel, view, null);
        await ActivateItemAsync(viewModel);
 }

And I active my child views like so:

 await ActivateItemAsync<ViewModel1>();
 await ActivateItemAsync<ViewModel2>();

This results in OnViewAttached and OnViewReady methods getting called on the child views but not OnViewLoaded. I understand that under normal circumstances OnViewLoaded should only be called the first time the view is loaded but I would expect OnViewLoaded to be called at least once but it is not.

What am I missing? Is there a better way to achieve my objective?

KasperSK commented 2 years ago

Hi @gerfen,

If I remember correctly OnViewLoaded is called the first time a view is loaded, OnViewAttached should be called right after and OnViewReady should be called when the view is has had its layout updated. This is triggered from ViewModelBinder.Bind. Can you make a repro of the issues so that we could debug it?

/KasperSK

gerfen commented 2 years ago

Hi @KasperSK,

I'm working on a repro. I hope to have something in a day or two.

vb2ae commented 2 years ago

@gerfen where you able to create a repo?

gerfen commented 2 years ago

@vb2ae Unfortunately I have not been able to create a repro yet. At the moment, I am contemplating going a different direction. I will close this issue for now and will reopen if appropriate.

gerfen commented 2 years ago

@KasperSK and @vb2ae - I was finally able to create a repro of this issue. There seems to be an issue integrating Avalon Dock with Caliburn.Micro. The repro of the issue can be found here: https://github.com/gerfen/CaliburnMicroAvalonDockBug. As a reminder, the OnViewAttached, OnViewReady and OnViewLoaded methods of the conducted view models are not called. I found a work around which allows the OnViewAttached and OnViewReady methods to be called but I have not been able to get OnViewLoaded to be called.

The work around looks like this:

    private async Task ActivateItemAsync<TViewModel>(CancellationToken cancellationToken = default)
            where TViewModel : Screen
        {

            // NOTE:  This is the hack to get OnViewAttached and OnViewReady methods to be called on conducted ViewModels.  Also note
            //   OnViewLoaded is not called.

            var viewModel = IoC.Get<TViewModel>();
            viewModel.Parent = this;
            viewModel.ConductWith(this);
            var view = ViewLocator.LocateForModel(viewModel, null, null);
            ViewModelBinder.Bind(viewModel, view, null);
            await ActivateItemAsync(viewModel, cancellationToken);
        }

I call this method like so in my Conductor implementation in the OnInitializeAsync method:

            await ActivateItemAsync<Tab1ViewModel>();
            await ActivateItemAsync<Tab2ViewModel>();
            await ActivateItemAsync<DesignSurfaceViewModel>();

The code also demonstrates another issue which I believe is related to OnViewLoaded never being called on the conducted view models. The view appears to be completely severed from the view model, almost like there are two instances of the view - one which is connected to the view model which is not shown in the visual tree and another which is disconnected from the view model but is displayed in the visual tree. I don't know this for sure but when I try to draw a shape on a Canvas control, nothing happens, even if I call a method on the View.

The UI looks like this:

image

The blue From View shape is drawn from the XAML for the view. The orange shape with the words From View just below it is drawn from the OnInitialized method of the view. When I click the Draw Shape button, another blue shape with the words From ViewModel should appear yet nothing happens.

The logs below show the various life cycle methods being called.

2022-08-01 11:16:57.111 -07:00 [Information] [CaliburnMicroAvalonDockBug.Bootstrapper] Application is starting.
2022-08-01 11:16:57.574 -07:00 [Information] [CaliburnMicroAvalonDockBug.Views.DesignSurfaceView] Drawing `From View` shape from the view's OnInitialized method.
2022-08-01 11:16:57.578 -07:00 [Information] [CaliburnMicroAvalonDockBug.ViewModels.DesignSurfaceViewModel] DesignSurfaceViewModel.OnViewAttached Called.
2022-08-01 11:16:58.662 -07:00 [Information] [CaliburnMicroAvalonDockBug.Views.DesignSurfaceView] Drawing `From View` shape from the view's OnInitialized method.
2022-08-01 11:16:58.670 -07:00 [Information] [CaliburnMicroAvalonDockBug.ViewModels.DesignSurfaceViewModel] DesignSurfaceViewModel.OnViewReady Called.
2022-08-01 11:17:01.102 -07:00 [Information] [CaliburnMicroAvalonDockBug.ViewModels.DesignSurfaceViewModel] DesignSurfaceViewModel.DrawShape called.
2022-08-01 11:17:01.106 -07:00 [Information] [CaliburnMicroAvalonDockBug.Views.DesignSurfaceView] DesignSurfaceView.AddControl called - adding Border to DesignSurfaceCanvas.
2022-08-01 11:17:02.813 -07:00 [Information] [CaliburnMicroAvalonDockBug.Bootstrapper] Application is exiting.
2022-08-01 11:18:01.813 -07:00 [Information] [CaliburnMicroAvalonDockBug.Bootstrapper] Application is starting.
2022-08-01 11:18:02.264 -07:00 [Information] [CaliburnMicroAvalonDockBug.Views.DesignSurfaceView] Drawing `From View` shape from the view's OnInitialized method.
2022-08-01 11:18:02.268 -07:00 [Information] [CaliburnMicroAvalonDockBug.Views.DesignSurfaceView] DesignSurfaceView.AddControl called - adding Border to DesignSurfaceCanvas.
2022-08-01 11:18:02.271 -07:00 [Information] [CaliburnMicroAvalonDockBug.ViewModels.DesignSurfaceViewModel] DesignSurfaceViewModel.OnViewAttached Called.
2022-08-01 11:18:03.290 -07:00 [Information] [CaliburnMicroAvalonDockBug.Views.DesignSurfaceView] Drawing `From View` shape from the view's OnInitialized method.
2022-08-01 11:18:03.294 -07:00 [Information] [CaliburnMicroAvalonDockBug.Views.DesignSurfaceView] DesignSurfaceView.AddControl called - adding Border to DesignSurfaceCanvas.
2022-08-01 11:18:03.303 -07:00 [Information] [CaliburnMicroAvalonDockBug.ViewModels.DesignSurfaceViewModel] DesignSurfaceViewModel.OnViewReady Called.

Note that the log message Drawing 'From View' shape from the view's OnInitialized method. appears twice yet only one orange shape appears in the visual tree:

image

I realize this may also be an issue with Avalon Dock but that fact that the OnViewLoaded method never gets called seems to point to Caliburn.Micro. Please let me know if I can provide any further detail.

Thanks!

gerfen commented 2 years ago

Another data point... If I use an updated version of Gemini (using the latest versions of CM and Avalon Dock) I can add shapes to a canvas without any issue. One of the main differences is that Gemini uses MEF. I'm not sure if that's the difference or if the there's some other internal plumbing which addresses the issue. What I can say is that all three of theOnView* methods are called in this order ->OnViewAttached, OnViewReady, followed by OnViewLoaded.

image

My updated Gemini code can be found here: https://github.com/gerfen/gemini/tree/di-spike

KasperSK commented 2 years ago

Hi @gerfen,

Thanks for the repo! I will have a look at this later today.

/Kasper

gerfen commented 2 years ago

I have a little more info. If I use binding via XAML (i.e. binding to ItemsSource on a ListView) then I can draw on the canvas