Search code examples
c#wpfmvvmcaliburn.micro

WPF Caliburn.Micro - Best way to navigate in Single Window Application


  1. My history:

I'm developing a WPF application, which will run in full screen on the touch screen. Navigation in my application can be done only by clicking a button on each page ("back" or "logout").

This is not a Universal App, but it looks like.

  1. Assumptions of the project:

    • Application will run on full screen mode in Windows 7 on the touch screen.
    • I'm using Caliburn.Micro MVVM framework.
  2. Problem and question:

I've got 1 window and 3 UserControl (and ViewModels) Concept art

 Window ShellView
     UserControl LoginView
     UserControl OrdersView
     UserControl OrderDetailView

When application starting, i'm set LoginView as default and load it by using CM Conductor ActivateItem method, but i don't know how to set another View from UserControl like LoginView

I have read: this question but this doesn't cover my case and this answer but it's to hard to understand for me.

My ideas:

  • make static method in ShellViewModel like:

ShellViewModel

public static void setOrdersView() {
    ActivateItem(new OrdersViewModel());
    // Error : An object reference is required for the non-static field, method, or property 'Caliburn.Micro.ConductorBase<object>.ActivateItem(object)
}
ShellViewModel.setOrdersView();
  • make listener in ShellViewModel and send event from child ViewModel ( but now i don't know how to achieve it)

Question: What is the best way to handle navigation in this case?

  1. Application architecture:

ShellView

<Window>
   <ContentControl x:Name="ActiveItem" />
</Window>

ShellViewModel

public class ShellViewModel : Conductor<object>, IShell
{

    public ShellViewModel()
    {
        LoadDefault();
    }    

    public void LoadDefault()
    {
        ActivateItem(new LoginViewModel());
    }
}

LoginView

<UserControl>
    <Button x:Name="Login" />
</UserControl>

LoginViewModel

public class LoginViewModel : PropertyChangedBase
{
    public void Login() {
        if (LoginManager.Login("User", "Password")) {
            // How to redirect user to OrdersView?
        }
    }
}

Solution

  • I have similar applications with one shell window and many activated views inside and some dialog windows. You should use EventAggregator pattern for these needs and Caliburn already has implementation.

    How to Achieve:

    Minimum Shell signature

    public class ShellViewModel : Conductor<object>,
        IHandle<ChangePageMessage>,
        IHandle<OpenWindowMessage>
    

    You need two fields inside (second one is for dialogs):

    public IEventAggregator EventAggregator { get; private set; }
    public IWindowManager WindowManager { get; private set; }
    

    I've set single instances of that objects through IoC. You can also define them as Singletons. EventAggregator needs subscription on object where IHandles are implemented.

    EventAggregator.Subscribe(this); //You should Unsubscribe when message handling is no longer needed
    

    Handlers implementation:

    public void Handle(ChangePageMessage message) {
        var instance = IoC.GetInstance(message.ViewModelType, null);//Or just create viewModel by type
        ActivateItem(instance);
    }
    
    public void Handle(OpenWindowMessage message) {
        var instance = IoC.GetInstance(message.ViewModelType, null);//Or just create viewModel by type
        WindowManager.ShowWindow(instance);
    }
    

    Messages for event aggregator can be only marker classes but sometimes it is useful to pass more parameters like ours OpenWindowMessage and ChangePageMessage classes - they are absolutely similar by content, so for example:

    public class OpenWindowMessage {
    
        public readonly Type ViewModelType;
    
        public OpenWindowMessage(Type viewModelType) {
            ViewModelType = viewModelType;
        }
    }
    

    All your viewModels can also subscribe to EventAggregator instance and Handle some messages for communication, or even for initial parameters. I have something like MyViewModelInitMessage classes almost for every viewModel, and just use two publish methods together.

    EventAggregator.Publish(new ChangePageMessage(typeof(MyViewModel)));
    EventAggregator.Publish(new MyViewModelInitMessage("...all needed parameters"));
    

    So when I publish those two - mine ViewModel will be activated, then it subscribes to EventAggregator(don't forget to do that or second message handling never occurs), and will handle its InitMessage right after.

    Now with EventAggregator you can send messages between all ViewModels that are currently subscribed to it.

    That seems pretty common solution.