MVVM Light 5.0:如何使用导航服务

MVVM Light 5.0: How to use the Navigation service

the latest release of MVVM Light note 中指出,MVVM Light 现在提供 "Navigation Service"。

但是我和我的朋友google找不到如何使用它。

我可以看到我可以向 ServiceLocator 请求 INavigationService,所以我知道如何请求转到另一个页面,但是:

  1. 我创建了一个新的 windows,我希望在其中为 "page" 保留一个特定区域,我该如何指定它?
  2. 如何指定所有可用页面?有什么我应该打电话的吗?
  3. INavigationService
  4. 的参数的格式是什么

这个库有官方文档吗?因为目前我发现它编码得很好并且工作正常,但是当我必须搜索如何使用它时,我从来没有找到 documentation/sample 展示如何使用,除了他的博客有一些条目。这非常令人沮丧。我找到的唯一文档是 this,我对 Pluralsight 不是很熟悉,但似乎必须按月订阅(作为个人,它试图在我的空闲时间进行应用,不可能)。

我不知道 mvvm light 是否提供导航功能。我用 contentControl 绑定实现它:

<xcad:LayoutDocumentPane>
     <xcad:LayoutDocument x:Name="DetailDoc" CanClose="False">
           <ContentControl Content="{Binding  DisplayedDetailViewModel}"/>
     </xcad:LayoutDocument>
</xcad:LayoutDocumentPane>

然后是视图模型属性。它继承自mvvm light ViewModelBase class.

public ViewModelBase DisplayedDetailViewModel
{
    get
    {
        return displayedDetailViewModel;
    }
    set
    {
        if (displayedDetailViewModel == value)
        {
            return;
        }
        displayedDetailViewModel = value;
        RaisePropertyChanged("DisplayedDetailViewModel");
    }
}

为了让内容控件知道它必须使用哪个用户控件,您可以在 app.xaml 中定义 DataTemplates :

 <Application.Resources>
    <ResourceDictionary>
        <!--
        We define the data templates here so we can apply them across the
        entire application.

        The data template just says that if our data type is of a particular
        view-model type, then render the appropriate view.  The framework
        takes care of this dynamically.  Note that the DataContext for
        the underlying view is already set at this point, so the
        view (UserControl), doesn't need to have it's DataContext set
        directly.
    -->
        <DataTemplate DataType="{x:Type viewModel:LoggerViewModel}">
            <views:LogView />
        </DataTemplate>
    </ResourceDictionary>
</Application.Resources>

LogView 是 UserControl。您只需将 LoggerViewModel 分配给 DisplayedDetailViewModel,Framework 就会完成工作。

是的,MvvmLight 在他们的最后一个版本 but they did't offer any implementation regarding Wpf 中引入了 NavigationService(您可以在 WP、Metroapps 中使用已实现的 NavigationService,..)但不幸的是没有Wpf,你需要自己实现, 这是我目前的做法 (credit)

首先 创建实现 MvvmLight INavigationService

的导航界面
public interface IFrameNavigationService : INavigationService
{
    object Parameter { get; }  
}

Parameter 用于在 ViewModels 之间传递对象,INavigationServiceGalaSoft.MvvmLight.Views 命名空间的一部分

然后像这样实现该接口

    class FrameNavigationService : IFrameNavigationService,INotifyPropertyChanged
    {
        #region Fields
        private readonly Dictionary<string, Uri> _pagesByKey;
        private readonly List<string> _historic;
        private string _currentPageKey;  
        #endregion
        #region Properties                                              
        public string CurrentPageKey
        {
            get
            {
                return _currentPageKey;
            }

            private  set
            {
                if (_currentPageKey == value)
                {
                    return;
                }

                _currentPageKey = value;
                OnPropertyChanged("CurrentPageKey");
            }
        }
        public object Parameter { get; private set; }
        #endregion
        #region Ctors and Methods
        public FrameNavigationService()
        {
            _pagesByKey = new Dictionary<string, Uri>();
            _historic = new List<string>();
        }                
        public void GoBack()
        {
            if (_historic.Count > 1)
            {
                _historic.RemoveAt(_historic.Count - 1);
                NavigateTo(_historic.Last(), null);
            }
        }
        public void NavigateTo(string pageKey)
        {
            NavigateTo(pageKey, null);
        }

        public virtual void NavigateTo(string pageKey, object parameter)
        {
            lock (_pagesByKey)
            {
                if (!_pagesByKey.ContainsKey(pageKey))
                {
                    throw new ArgumentException(string.Format("No such page: {0} ", pageKey), "pageKey");
                }

                var frame = GetDescendantFromName(Application.Current.MainWindow, "MainFrame") as Frame;

                if (frame != null)
                {
                    frame.Source = _pagesByKey[pageKey];
                }
                Parameter = parameter;
                _historic.Add(pageKey);
                CurrentPageKey = pageKey;
            }
        }

        public void Configure(string key, Uri pageType)
        {
            lock (_pagesByKey)
            {
                if (_pagesByKey.ContainsKey(key))
                {
                    _pagesByKey[key] = pageType;
                }
                else
                {
                    _pagesByKey.Add(key, pageType);
                }
            }
        }

        private static FrameworkElement GetDescendantFromName(DependencyObject parent, string name)
        {
            var count = VisualTreeHelper.GetChildrenCount(parent);

            if (count < 1)
            {
                return null;
            }

            for (var i = 0; i < count; i++)
            {
                var frameworkElement = VisualTreeHelper.GetChild(parent, i) as FrameworkElement;
                if (frameworkElement != null)
                {
                    if (frameworkElement.Name == name)
                    {
                        return frameworkElement;
                    }

                    frameworkElement = GetDescendantFromName(frameworkElement, name);
                    if (frameworkElement != null)
                    {
                        return frameworkElement;
                    }
                }
            }
            return null;
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion
    }

上面代码中的MainFrame是一个简单的Frame控件的x:Name定义在Xaml用于页面间导航(根据您的需要定制)

第二个:在viewmodellocator中,初始化你的导航服务(SetupNavigation()),这样你就可以在你的视图模型中使用它了:

static ViewModelLocator()
{
     ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);

     SetupNavigation();

     SimpleIoc.Default.Register<MainViewModel>();
     SimpleIoc.Default.Register<LoginViewModel>();
     SimpleIoc.Default.Register<NoteViewModel>();            
 }
 private static void SetupNavigation()
 {
     var navigationService = new FrameNavigationService();
     navigationService.Configure("LoginView", new Uri("../Views/LoginView.xaml",UriKind.Relative));
     navigationService.Configure("Notes", new Uri("../Views/NotesView.xaml", UriKind.Relative));            

      SimpleIoc.Default.Register<IFrameNavigationService>(() => navigationService);
 }

第三: 最后,使用服务,例如

 public LoginViewModel(IFrameNavigationService navigationService)
 {
      _navigationService = navigationService; 
 }
...
_navigationService.NavigateTo("Notes",data);
..

编辑

可以在此 repo.

找到一个明确的示例

我宁愿使用 ViewModelFirst 导航服务。

在我看来,创建一对新的 View/ViewModel.

为此你需要一些东西 :

首先是一个 NavigableViewModel 抽象 class,其中包含一些处理两种导航方式的方法。您所有的 viewModel 都将继承自此 class :

NavigableViewModel.cs

public abstract class NavigableViewModel : ViewModelBase
{
    public abstract void OnNavigatedTo(object parameter = null);
    public abstract void OnNavigatingTo(object parameter = null);
}

包含导航发生的框架的主窗口,只需考虑使用 NavigationUIVisibility="Hidden" 隐藏默认导航控件:

MainWindow.xaml

<Window x:Class="YourProject.Views.MainWindow" 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:SS3DViewModelFirstMvvmLightProject"
        mc:Ignorable="d"
        DataContext="{Binding Main, Source={StaticResource Locator}}"
        Title="MainWindow" Height="350" Width="525">
        <-- Just remeber to replace x:Class="YourProject.Views.MainWindow" with your actual project path-->
        <Frame  x:Name="Frame"  NavigationUIVisibility="Hidden">

        </Frame>
</Window>

还有一些处理 ViewModel 更改的代码(允许我们通知每个页面的 viewModel):

MainWindow.xaml.cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        ((MainViewModel)this.DataContext).ShowFirstView(); // we need to have our view loaded to start navigating
        Frame.LoadCompleted += (s, e) => UpdateFrameDataContext();
        Frame.DataContextChanged += (s, e) => UpdateFrameDataContext();
    }

    private void UpdateFrameDataContext()
    {
        Page view = (Page)Frame.Content;
        if (view != null)
        {
            view.DataContext = Frame.DataContext;
        }
    }
}

在您的 MainViewModel 中,这个导航到您的第一个 ViewModel(此处为 LoginViewModel)的小方法:

MainViewModel.cs

public class MainViewModel : ViewModelBase
    {
        public MainViewModel()
        {
          
        }

        public void ShowFirstView()
        {
            ServiceLocator.Current.GetInstance<ViewModelFirstNavigationService>().NavigateTo<LoginViewModel>();
            //To navigate wherever you want you just need to call this method, replacing LoginViewModel with YourViewModel
        }
    }

为了让这个 ServiceLocator 调用正常工作,我们需要向 ViewModelLocator 添加一些东西:

ViewModelLocator.cs

 public class ViewModelLocator
    {
        public ViewModelLocator()
        {
            ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
            SimpleIoc.Default.Register<MainViewModel>();
            ViewModelFirstNavigationService navService = new ViewModelFirstNavigationService(Main);
            SimpleIoc.Default.Register<LoginViewModel>();
            navService.AddNavigableElement(SimpleIoc.Default.GetInstance<LoginViewModel>);
            // so whenever you want to add a new navigabel View Model just add these lines here
            // SimpleIoc.Default.Register<YourViewModel>();
            // navService.AddNavigableElement(SimpleIoc.Default.GetInstance<YourViewModel>);
            SimpleIoc.Default.Register<ViewModelFirstNavigationService>(() => navService);
        }

        public MainViewModel Main
        {
            get
            {
                return ServiceLocator.Current.GetInstance<MainViewModel>();
            }
        }
        
        public static void Cleanup()
        {
        }
    }

现在一切就绪,让我们添加系统的核心,导航服务(这是棘手的部分):

ViewModelFirstNavigationService

public class ViewModelFirstNavigationService
    {
        private Dictionary<Type, Uri> _registeredViews;
        private Dictionary<Type, Func<NavigableViewModel>> _registeredViewModels;
        private List<string> _allXamlPages;
        private MainViewModel _mainContainerViewModel;
        public NavigableViewModel CurrentViewModel;

        public ViewModelFirstNavigationService(MainViewModel mainContainerViewModel)
        {
            _mainContainerViewModel = mainContainerViewModel;
            _registeredViews = new Dictionary<Type, Uri>();
            _registeredViewModels = new Dictionary<Type, Func<NavigableViewModel>>();
            _allXamlPages = GetAllXamlPages();
        }

        private List<string> GetAllXamlPages()
        {
            // this part is a bit tricky. We use it to find all xaml pages in the current project.
            // so you need to be sure that all your pages you want to use with your viewmodles need to end with page.xaml
            // Example : LoginPage.xaml will work fine. Parameters.xaml won't.
            System.Reflection.Assembly viewModelFirstProjectAssembly;
            viewModelFirstProjectAssembly = System.Reflection.Assembly.GetExecutingAssembly();
            var stream = viewModelFirstProjectAssembly.GetManifestResourceStream(viewModelFirstProjectAssembly.GetName().Name + ".g.resources");
            var resourceReader = new ResourceReader(stream);
            List<string> pages = new List<string>();
            foreach (DictionaryEntry resource in resourceReader)
            {
                Console.WriteLine(resource.Key);
                string s = resource.Key.ToString();
                if (s.Contains("page.baml"))
                {
                    pages.Add(s.Remove(s.IndexOf(".baml")));
                }
            }
            return pages;
        }

        private Type ResolveViewModelTypeFromSingletonGetterFunc<T>(Func<T> viewModelSingletonGetterFunc)
        {
            MethodInfo methodInfo = viewModelSingletonGetterFunc.Method;
            return methodInfo.ReturnParameter.ParameterType;
        }

        private Uri ResolvePageUriFromViewModelType(Type viewModelType)
        {
            string pageName = String.Empty;
            int index = viewModelType.Name.IndexOf("ViewModel");
            pageName = viewModelType.Name.Remove(index);
            string pagePath = String.Format("{0}.xaml", _allXamlPages.Where(page => page.Contains(pageName.ToLower())).FirstOrDefault());
            string cleanedPath = pagePath.Remove(0, "views/".Length); //obviously for this to work you need to have your views in a Views folder at the root of the project. But you are alowed yo reat sub folders in it
            return new Uri(cleanedPath, UriKind.Relative);
        }


        public void AddNavigableElement(Func<NavigableViewModel> viewModelSingletonGetter)
        {
            //Where the magic happens !
            //If your are wondering why a Func, it's because we want our viewmodels to be instantiated only when we need them via IOC.
            //First we ge the type of our viewmodel to register for the Func.
            Type vmType = ResolveViewModelTypeFromSingletonGetterFunc(viewModelSingletonGetter);
            Uri uriPage = ResolvePageUriFromViewModelType(vmType);
            _registeredViews.Add(vmType, uriPage);
            _registeredViewModels.Add(vmType, viewModelSingletonGetter);
        }

        public void NavigateTo<GenericNavigableViewModelType>(object parameter = null)
        {
            Type key = typeof(GenericNavigableViewModelType);
            NavigateTo(key, parameter);
        }

        public void NavigateTo(Type key, object parameter = null)
        {
            CurrentViewModel?.OnNavigatingTo(parameter);
            CurrentViewModel = _registeredViewModels[key].Invoke();
            Uri uri = _registeredViews[key];
            ((MainWindow)Application.Current.MainWindow).Frame.Source = uri;
            ((MainWindow)Application.Current.MainWindow).Frame.DataContext = CurrentViewModel;
            CurrentViewModel.OnNavigatedTo(parameter);
        }

    }

现在一切正常!欢呼!让我们用我们的例子 LoginViewModel 来演示(它只包含一个漂亮的黑色方块中的地狱世界):

LoginPage.xaml

<Page x:Class="YourProject.Views.LoginPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
      xmlns:local="clr-namespace:SS3DViewModelFirstMvvmLightProject.Views"
      mc:Ignorable="d" 
      d:DesignHeight="300" d:DesignWidth="300"
      Title="LoginPage">
    <Grid Background="Gray">
        <Label Content="{Binding HelloWorld}" Foreground="White" Background="Black" Width="150" Height="150"></Label>
    </Grid>
</Page>

及其视图模型:

LoginViewModel.cs

public class LoginViewModel : NavigableViewModel
    {
        private string _helloWorld;
        public string HelloWorld
        {
            get
            {
                return _helloWorld;
            }
            set
            {
                _helloWorld = value;
                RaisePropertyChanged(() => HelloWorld);
            }
        }

        public LoginViewModel()
        {
            HelloWorld = "Hello World";
        }

        public override void OnNavigatedTo(object parameter = null)
        {
          // whatever you want to happen when you enter this page/viewModel
        }

        public override void OnNavigatingTo(object parameter = null)
        {
            // whatever you want to happen when you leave this page/viewmodel
        }
    }

我承认您首先需要一些代码。但是当一切正常时,您最终会得到一个非常易于使用的系统。

想导航到某个 viewModel 吗? 只需使用 myNavigationService.NavigateTo(someParam);

想要添加一对新的 View / ViewModel 吗? 只需将您的 viewModel 添加到某个 IOC 容器中(在我的项目中,我使用自己的 ioc,这允许我随时卸载我的 viewmodels 并提供一些精细的导航堆栈)并为其提供导航服务。