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

26
MVVM Light最新版本的说明中指出,MVVM Light现在提供了“导航服务”。但是我和我的朋友谷歌都找不到如何使用它的方法。我可以看到我可以向ServiceLocator请求INavigationService,所以我知道如何请求前往另一个页面,但是:
  1. 我创建了一个新窗口,在那里我希望为“页面”保留一个特定区域,我该如何指定?
  2. 我如何指定所有可用的页面?有什么需要调用的吗?
  3. INavigationService的参数的格式会是什么样子?

这个库有官方文档吗?目前我发现它的代码编写得很好,而且运行良好,但当我需要搜索如何使用它时,除了他的博客上有一些条目外,我从未找到文档或示例。这非常令人沮丧。我找到的唯一文档是this,我不太熟悉Pluralsight,但似乎必须订阅每月付费(作为个人,我正在利用空闲时间开发应用程序,这是不可能的)。


你看过这个链接吗?https://marcominerva.wordpress.com/2014/10/10/navigationservice-in-mvvm-light-v5/ - Eric Bole-Feysot
完全不是。但目前他们正在执行一个new NavigationService(),这是一个内部构造函数,对我不可用。 - J4N
1
我刚看到你文章中的一条评论,其中提到“MVVM Light在WPF中没有提供INavigationService的实现,因为这个平台没有标准的导航系统。”但是如果WPF没有导航系统,我们该如何从一个用户控件过渡到另一个呢? - J4N
在WPF中使用ContentControl,并将“content”属性绑定到ViewModel属性。 - Eric Bole-Feysot
那么您确认在MVVM Light中没有实现这种导航的方法吗?您能否创建一个带有更多细节的答案呢?因为我知道如何进行绑定,但是绑定的类型是什么?另一个视图模型如何知道它必须将什么设置为“ContentControl”? - J4N
3个回答

46

是的,MvvmLight在他们的最新版本中引入了NavigationService 但是他们没有提供关于Wpf的任何实现(你可以使用已经实现的NavigationService在WP,Metroapps等项目上)但不幸的是不支持Wpf,你需要自己实现,这里是我当前的实现方法(来源

首先创建一个导航界面,实现MvvmLightINavigationService

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是在Xaml中定义的简单Frame控件的x:Name,用于在页面之间导航(根据您的需求自定义)。
其次,在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);
..

编辑

可以在此存储库中找到一个明确的示例。


1
非常有趣。所以如果我理解正确的话,我们期望在MainWindow上找到一个名为MainFrame的“Frame”,并将内容放入其中。ViewModel还有责任从服务中获取其潜在参数。但是目前,如果我们返回,我们如何知道应该将哪个参数分配给服务? - J4N
2
没错,通过使用_navigationService.Parameter,您可以在ViewModel之间传递对象。只需在所有ViewModel的Ctors中传递一个IFrameNavigationService即可。您可以将GoBack方法更改为接受参数以返回(Object parameter),该参数将传递给上一个视图.. - SamTh3D3v
2
[NotifyPropertyChangedInvocator] 只是 Resharper 的一个属性,对吧? - Carth
2
请问我如何从接收 ViewModel 中获取 _navigationService.NavigateTo("Notes",data) 的返回对象/值? - Odin
1
嗨@Rod,我最终构建了一个更明确的示例来回答某人的问题,你可能会发现这很有用;) https://github.com/SamTheDev/SampleMvvmLightNavigation - SamTh3D3v
显示剩余8条评论

4
我更愿意使用ViewModelFirst导航服务。
在我看来,这更容易使用,并且在创建新的View / ViewModel时需要添加的代码要少得多。
为此,您需要一些东西:
首先是NavigableViewModel抽象类,其中包含一些方法来处理双向导航。您的所有viewModels都将继承自此类:
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来演示(它只包含一个漂亮的黑色正方形中的helloworld):
Login页面.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>

其 ViewModel 如下:

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吗? 只需使用我的导航服务.NavigateTo(someParam)。
想要添加新的视图/视图模型对吗? 只需将您的视图模型添加到某个IOC容器(在我的项目中,我使用自己的IOC,它允许我在需要时卸载我的视图模型并提供一些细粒度的导航堆栈),然后将其赋予您的导航服务即可。

你如何使用这个示例在主窗口上创建新的窗口? - Eylon

2

我不知道在mvvm light中是否有导航功能。我使用ContentControl绑定来实现它:

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

然后是viewmodel属性。它继承自mvvm light ViewModelBase类。

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是用户控件。您只需将LoggerViewModel分配给DisplayedDetailViewModel,框架就会完成工作。


但是内容控件如何知道它必须使用哪个用户控件来显示这个模型? - J4N
好的,我明白了,但是我们不应该使用来自 NavigationWindows(https://msdn.microsoft.com/en-us/library/ms750478%28v=vs.110%29.aspx#The_NavigationWindow_Class)的东西吗?还是这只适用于 Silverlight? - J4N
我不知道。我找到了这个解决方案并实现了它。我觉得它非常聪明,而且很适合mvvm模式。 - Eric Bole-Feysot
你怎么向一个ViewModel询问如何转到另一个ViewModel? - J4N
你需要将想要展示的视图模型分配给DisplayedDetailViewModel属性。 - Eric Bole-Feysot

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接