Navigation with Caliburn Micro

2020-05-24 11:36发布

问题:

I'm playing around with Caliburn.Micro and have a very simple application now.

It has an AppView, which actually has a ContentControl for a NavigationBar, an InnerView and a StatusBar.

Now I want to handle Navigation between different inner views.

Right now I use the eventaggregator to publish a NavigationEvent, which should switch the innerview of the mainwindow to another view.

Here is my call to Publish (all InnerViews have the same baseclass which has an IEventAggregator)

public void NavigateOverview()
{
    base._eventAggregator.Publish(new NavigateEvent("OverviewViewModel"));
}

Right now I pass a string to the AppViewModel, which handles the NavigateEvent:

        public void Handle(NavigateEvent navigate)
        {
            InnerViewModel target;

            switch (navigate.TargetViewModel)
            {
                case "SelectProjectViewModel":
                {
                    target = new SelectProjectViewModel(_eventAggregator);
                    break;
                }
                case "OverviewViewModel":
                {
                    target = new OverviewViewModel(_eventAggregator);
                    break;
                }
                default:
                {
                    throw new InvalidOperationException("no target type found");
                }
            }

            this.CurrentInnerViewModel = target;
        }

Passing strings works, but is errorprone and not very clean.

What is the Caliburn way of handling that? Is that what a Conductor should do?

回答1:

Why not just pass a type instead? That way there are no magic strings

e.g.

public void NavigateOverview()
{
    base._eventAggregator.Publish(new NavigateEvent(typeof(OverviewViewModel)));
}

then:

    public void Handle(NavigateEvent navigate)
    {
        InnerViewModel target;

        // EDIT: Remove the case (only works with integral types so you can't use typeof etc)
        // but you could do this with standard conditional logic

        this.CurrentInnerViewModel = target;
    }

Edit 2:

Ok since you asked about building into CMs IoC, here is an example for using the IoC with Castle Windsor and a solution for passing additional parameters to navigation (borrowing from EventAggregator)

The bootstrapper just needs a few bits and pieces to config the container:

public class AppBootstrapper : Bootstrapper<ShellViewModel>
{
    // The Castle Windsor container
    private IWindsorContainer _container;

    protected override void Configure()
    {
        base.Configure();

        // Create the container, install from the current assembly (installer code shown in next section below)
        _container = new WindsorContainer();
        _container.Install(FromAssembly.This());
    }

    // Matches up with Windsors ResolveAll nicely
    protected override IEnumerable<object> GetAllInstances(Type service)
    {
        return (IEnumerable<object>)_container.ResolveAll(service);
    }

    // Matches up with Windsors Resolve
    protected override object GetInstance(Type service, string key)
    {
        return string.IsNullOrEmpty(key) ? _container.Resolve(service) : _container.Resolve(key, service);
    }

    // Windsor doesn't do property injection by default, but it's easy enough to get working:
    protected override void BuildUp(object instance)
    {
        // Get all writable public properties on the instance we will inject into
        instance.GetType().GetProperties().Where(property => property.CanWrite && property.PropertyType.IsPublic)
        // Make sure we have a matching service type to inject by looking at what's registered in the container
                                          .Where(property => _container.Kernel.HasComponent(property.PropertyType))
        // ...and for each one inject the instance
                                          .ForEach(property => property.SetValue(instance, _container.Resolve(property.PropertyType), null));
    }
}

The Windsor Installer for CM will probably be as simple as:

public class CaliburnMicroInstaller : IWindsorInstaller
{
    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        // Register the window manager
        container.Register(Component.For<IWindowManager>().ImplementedBy<WindowManager>());

        // Register the event aggregator
        container.Register(Component.For<IEventAggregator>().ImplementedBy<EventAggregator>());
    }
}

I also have a navigation service interface to aid with application navigation:

public interface INavigationService
{
    void Navigate(Type viewModelType, object modelParams);
}

Which is implemented by NavigationService (show you that in a sec)

That needs a Windsor installer too:

public class NavigationInstaller : IWindsorInstaller
{
    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        container.Register(Component.For<INavigationService>().ImplementedBy<NavigationService>());
    }
}

The NavigationService works much like EventAggregator in that the type that exposes navigation arguments should implement a generic interface for each argument class that it can receive...

The interface looks like this (borrowing heavily from EventAggregator):

// This is just to help with some reflection stuff
public interface IViewModelParams { }

public interface IViewModelParams<T> : IViewModelParams        
{
    // It contains a single method which will pass arguments to the viewmodel after the nav service has instantiated it from the container
    void ProcessParameters(T modelParams);
}

example:

public class ExampleViewModel : Screen, 
    // We can navigate to this using DefaultNavigationArgs...
    IViewModelParams<DefaultNavigationArgs>, 
    // or SomeNavigationArgs, both of which are nested classes...
    IViewModelParams<SomeOtherNavigationArgs>
{
    public class DefaultNavigationArgs
    {
        public string Value { get; private set; }

        public DefaultNavigationArgs(string value)
        {
            Value = value;
        }
    }

    public class OtherNavigationArgs
    {
        public int Value { get; private set; }

        public DefaultNavigationArgs(int value)
        {
            Value = value;
        }
    }

    public void ProcessParameters(DefaultNavigationArgs modelParams)
    {            
        // Do something with args
        DisplayName = modelParams.Value;
    }

    public void ProcessParameters(OtherNavigationArgs modelParams)
    {            
        // Do something with args. this time they are int!
        DisplayName = modelParams.Value.ToString();
    }
}

This leads to some strongly typed navigation (e.g. refactor friendly!)

NavigationService.Navigate(typeof(ExampleViewModel), new ExampleViewModel.DefaultNavigationArgs("hello"));

or

NavigationService.Navigate(typeof(ExampleViewModel), new ExampleViewModel.OtherNavigationArgs(15));

It also means that the ViewModel is still in control of it's own navigation parameters

Ok back to Windsor for a sec; obviously we need to install any views from our views namespace - Windsors fluent API makes this pretty easy:

public class ViewInstaller : IWindsorInstaller
{
    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        // The 'true' here on the InSameNamespaceAs causes windsor to look in all sub namespaces too
        container.Register(Classes.FromThisAssembly().InSameNamespaceAs<ShellViewModel>(true));
    }
}

Ok now the NavigationService implementation:

public class NavigationService : INavigationService
{
    // Depends on the aggregator - this is how the shell or any interested VMs will receive
    // notifications that the user wants to navigate to someplace else
    private IEventAggregator _aggregator;

    public NavigationService(IEventAggregator aggregator)
    {
        _aggregator = aggregator;
    }

    // And the navigate method goes:
    public void Navigate(Type viewModelType, object modelParams)
    {
        // Resolve the viewmodel type from the container
        var viewModel = IoC.GetInstance(viewModelType, null);

        // Inject any props by passing through IoC buildup
        IoC.BuildUp(viewModel);

        // Check if the viewmodel implements IViewModelParams and call accordingly
        var interfaces = viewModel.GetType().GetInterfaces()
               .Where(x => typeof(IViewModelParams).IsAssignableFrom(x) && x.IsGenericType);

        // Loop through interfaces and find one that matches the generic signature based on modelParams...
        foreach (var @interface in interfaces)
        {
            var type = @interface.GetGenericArguments()[0];
            var method = @interface.GetMethod("ProcessParameters");

            if (type.IsAssignableFrom(modelParams.GetType()))
            {
                // If we found one, invoke the method to run ProcessParameters(modelParams)
                method.Invoke(viewModel, new object[] { modelParams });
            }
        }

        // Publish an aggregator event to let the shell/other VMs know to change their active view
        _aggregator.Publish(new NavigationEventMessage(viewModel));
    }
}

Now the shell can just handle the aggregator message and activate the new injected and additionally configured VM

public class ShellViewModel : Conductor<IScreen>, IHandle<NavigationEventMessage>
{
    private IEventAggregator _aggregator;
    private INavigationService _navigationService;

    public ShellViewModel(IEventAggregator aggregator, INavigationService _navigationService)
    {
        _aggregator = aggregator;
        _aggregator.Subscribe(this);

        _navigationService.Navigate(typeof (OneSubViewModel), null);
    }

    public void Handle(NavigationEventMessage message)
    {
        ActivateItem(message.ViewModel);
    }
}

Actually I constrain the navigation to just IScreen implementations so my NavigationEventMessage actually looks like this:

public class NavigationEventMessage
{
    public IScreen ViewModel { get; private set; }

    public NavigationEventMessage(IScreen viewModel)
    {
        ViewModel = viewModel;
    }
}

This is because I always want lifecycle for my child viewmodels