Подтвердить что ты не робот

MVVM Light 5.0: как пользоваться навигационной службой

В последнем выпуске заметки MVVM Light было указано, что MVVM Light теперь предоставляет "Службу навигации".

Но я и мой друг Google не можем найти, как его использовать.

Я вижу, что могу попросить INavigationService для ServiceLocator, поэтому я вижу, как я могу попросить перейти на другую страницу, но:

  1. Я создал новые окна, где я ожидаю зарезервировать определенную зону для "страницы", как мне это указать?
  2. Как мне указать все доступные страницы? Мне нужно позвонить?
  3. Каков будет формат параметров, переданных INavigationService

Есть ли официальная документация для этой библиотеки? Потому что в настоящее время я нахожу его хорошо закодированным и работающим нормально, но когда мне приходится искать, как его использовать, я никогда не нахожу документацию/пример, показывающий, как это сделать, кроме его блога, в котором есть какая-то запись. Это очень расстраивает. Единственная документация, которую я нашел, это то, что я не очень хорошо знаком с Pluralsight, но кажется, что необходимо брать ежемесячную подписку (что как частное лицо, которое пытается подать заявку в свободное время, невозможно).

4b9b3361

Ответ 1

Да, MvvmLight представил NavigationService в своей последней версии, но они не предлагали никакой реализации в отношении Wpf (вы можете использовать реализованный NavigationService в WP, Metroapps,..), но, к сожалению, не в Wpf, вам нужно реализовать это самостоятельно, здесь как я сейчас это делаю (кредит)

сначала создайте интерфейс навигации, который MvvmLight INavigationService

public interface IFrameNavigationService:INavigationService
    {
        object Parameter { get; }  
    }

Parameter используется для передачи объектов между ViewModels, а INavigationService является частью пространства имен GalaSoft.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 в приведенном выше коде - это x: Имя простого Xaml управления Frame определенного в 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);
..

РЕДАКТИРОВАТЬ

Явный образец можно найти в этом репо.

Ответ 2

Я не знаю, доступна ли функция навигации в свете mvvm. Я применил его с привязкой 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");
        }

Чтобы элемент управления содержимым знал, какой пользовательский элемент управления он должен использовать, вы определяете DataTemplates в app.xaml:

 <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 DataContext set
        directly.
    -->
        <DataTemplate DataType="{x:Type viewModel:LoggerViewModel}">
            <views:LogView />
        </DataTemplate>

LogView - это UserControl. Вам просто нужно назначить LoggerViewModel DisplayDetailViewModel, и Framework выполнит эту работу.

Ответ 3

Я бы предпочел использовать навигационную службу 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);
}

A MainWindow, содержащий фрейм, в котором происходит перемещение, просто подумайте о том, чтобы скрыть навигационные элементы по умолчанию с помощью 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>

И какой-то код для обработки изменений в ViewModels (чтобы мы уведомили каждую страницу своего видаModel):

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 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 в черном квадрате):

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>

И его 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? Просто используйте myNavigationService.NavigateTo(someParam);

Хотите добавить новую пару View/ViewModel? Просто добавьте свой viewModel в какой-либо контейнер IOC (в моих проектах я использую свой собственный ioc, который позволяет мне выгружать мои модели просмотра всякий раз, когда я хочу, и давать какой-то прекрасный стек навигации) и предоставлять ему навигационную службу.