Navigation with Caliburn Micro

前端 未结 1 1397

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

相关标签:
1条回答
  • 2021-02-04 22:31

    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

    0 讨论(0)
提交回复
热议问题