canton7 / Stylet

A very lightweight but powerful ViewModel-First MVVM framework for WPF for .NET Framework and .NET Core, inspired by Caliburn.Micro.
MIT License
995 stars 144 forks source link

Conductor.OnActivate() shoudl not be required to call base.OnActivate() #378

Closed Yaevh closed 1 year ago

Yaevh commented 1 year ago

Description I've just stumbled on the following scenario:

I have a conductor, MyConductor. In its constructor, I immediately assign an ActiveItem. I expect that when MyConductor is activated, its ActiveItem will also be activated - and most of the times it works exactly that way.

However, if I override MyConductor.OnActivate(), the children are never activated. That's because children activation happens in Conductor.OnActivated(), and that method is overridden by MyConductor. So I have remember to call base.OnActivate() manually, or my ActiveItem won't get activated properly - which is an anti-pattern, see https://en.wikipedia.org/wiki/Call_super.

The same is true for OnDeactivate() and OnClose(): overriding these methods on MyConductor without remembering to manually call the base implementations results in ActiveItem failing to deactivate/close properly.

To Reproduce

var myConductor = new MyConductor();
ScreenExtensions.TryActivate(myConductor);

public class MyConductor : Conductor<IScreen>
{
    public MyConductor()
    {
        ActivateItem(new MyChild());
    }

    protected override void OnActivate()
    {
        //base.OnActivate(); <- I have to remember to call base here, or ActiveItem won't actually get activated
    }
}

public class MyChild : Screen
{
    protected override void OnActivate()
    {
        // never gets called!
    }
}

Version Info

Additional Info The solution could be either:

  1. Instead of overriding OnActivate()/OnDeactivate()/OnClose() methods, Conductor should rather listen for its own Activated/Deactivated/Closed events and change state of ActiveItem from there
  2. Conductor should provide its own implementation of Activate(), Deactivate() and Close() and call Screen.TryActivate(ActiveItem) etc. from there

I'm willing to implement either one of the solutions, but I want to know your opinion first :)

canton7 commented 1 year ago

Eh, needing to call the overridden method is common enough in .NET: you should always do it as a matter of habit. It also gives you control as to if and when to invoke the base method's behaviour, which people might be relying on.

At this point, any change here would be a breaking change though, I'm afraid.

Yaevh commented 1 year ago

I don't think the first solution would actually be a breaking change. Public API remains the same, and even when the users have indeed overridden Conductor.OnActivate() and called base.OnActivate(), their code would still compile and work as intended, but the base call would simply be a no-op, as there will be nothing to do in base.OnActivate().

The only publicly visible change is that from now on, ActiveItem.Activate() would always be activated after its parent Conductor.OnActivate() has returned; while until now, the users could potentially select the order of activation by deciding whether to call base.OnActivate() either before or after they did their work in Conductor.OnActivate().

I've created PR #379 with the first solution, but I'm not trying to force the issue. Please feel free to close the PR if you think it's too risky.

canton7 commented 1 year ago

The breaking change is that people who don't call the base method or who rely on the sequencing of calling the base method, have behaviour they potentially rely on changed.

It's common in wpf that you call the base method in your override - look at any OnXCC method on Window or UserControl. I don't think Stylet is taking an unusual approach here.

canton7 commented 1 year ago

Also, I think there's a thread dispatch when the events are called?