Навигация по страницам с помощью MVVM в приложении Store

У меня серьезная головная боль с этой проблемой. Мне действительно не нравятся приложения для хранения, но я вынужден использовать его в этом случае. Я работал с XAML всего несколько недель.

Мой вопрос: Как я могу назвать RelayCommand в моем ViewModel (из моего представления, конечно), который изменит страницу на моем представлении? И еще лучше, измените его с помощью URI, чтобы я мог передать параметр команды в файл.

Я полностью потерял это. В настоящее время я использую this.Frame.Navigate(type type) в коде просмотра для навигации по страницам.

Я бы действительно, и я имею в виду ДЕЙСТВИТЕЛЬНО оценить описание от a до z о том, что делать в этом случае.

Я предполагаю, что мог бы сделать что-то вроде построения framecontainer на моем представлении и отправить его в мою ViewModel и оттуда переместить текущий кадр в другой. Но я не уверен, как это работает в приложениях Store.

Мне очень жаль отсутствие хороших вопросов, но я нахожусь в крайнем сроке, и мне нужно, чтобы мой взгляд был подключен к моей ViewModel надлежащим образом. Мне не нравится иметь и представление codebehind, а также Код ViewModel.

Ответ 1

Есть два способа сделать это, простой способ - передать действие команды реле из представления в модель представления.

public MainPage()
{
  var vm = new MyViewModel();
  vm.GotoPage2Command = new RelayCommand(()=>{ Frame.Navigate(typeof(Page2)) });
  this.DataContext = vm;
}

<Button Command={Binding GoToPage2Command}>Go to Page 2</Button>

Другой способ - использовать IocContainer и DependencyInjection. Этот подход является более замкнутым.

Нам нужен интерфейс для страницы навигации, чтобы нам не нужно было ссылаться или ничего знать о PageX или любом элементе пользовательского интерфейса, предполагая, что ваша модель просмотра находится в отдельном проекте, который ничего не знает о пользовательском интерфейсе.

Проект ViewModel:

  public interface INavigationPage
  {
    Type PageType { get; set; }
  }

  public interface INavigationService
  {
    void Navigate(INavigationPage page) { get; set; }
  }



public class MyViewModel : ViewModelBase
  {
    public MyViewModel(INavigationService navigationService, INavigationPage page)
    {
      GotoPage2Command = new RelayCommand(() => { navigationService.Navigate(page.PageType); })
    }

    private ICommand GotoPage2Command { get; private set; }
  }

Проект пользовательского интерфейса:

  public class NavigationService : INavigationService
    {
       //Assuming that you only navigate in the root frame
       Frame navigationFrame = Window.Current.Content as Frame;
       public void Navigate(INavigationPage page)
       {
          navigationFrame.Navigate(page.PageType);
       }
    }

public abstract class NavigationPage<T> : INavigationPage
{
   public NavigationPage()
   {
      this.PageType = typeof(T);
   }
}

public class NavigationPage1 : NavigationPage<Page1> { }


public class MainPage : Page
{
   public MainPage()
   {
      //I'll just place the container logic here, but you can place it in a bootstrapper or in app.xaml.cs if you want. 
      var container = new UnityContainer();
      container.RegisterType<INavigationPage, NavigationPage1>();
      container.RegisterType<INavigationService, NavigationService>();
      container.RegisterType<MyViewModel>();

      this.DataContext = container.Resolve<MyViewModel>();       
   }
}

Ответ 2

Как говорит Скотт, вы можете использовать NavigationService. Я бы сначала создал интерфейс, который не нужен в этом примере, но будет полезен, если вы в будущем будете использовать Injection Dependency (хорошее решение с режимами просмотра и сервисами):)

INavigationService:

public interface INavigationService
{
    void Navigate(Type sourcePage);
    void Navigate(Type sourcePage, object parameter);
    void GoBack();
}

NavigationService.cs наследует INavigationService вам понадобятся следующие пространства имен

using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;


public sealed class NavigationService : INavigationService
{
    public void Navigate(Type sourcePage)
    {
        var frame = (Frame)Window.Current.Content;
        frame.Navigate(sourcePage);
    }

    public void Navigate(Type sourcePage, object parameter)
    {
        var frame = (Frame)Window.Current.Content;
        frame.Navigate(sourcePage, parameter);
    }

    public void GoBack()
    {
        var frame = (Frame)Window.Current.Content;
        frame.GoBack();
    }
}

Simple ViewModel, чтобы показать пример RelayCommand. NB Перейдите к другой странице (Page2.xaml) с помощью DoSomething RelayCommand.

MyViewModel.cs

public class MyViewModel : INotifyPropertyChanged
{
    private INavigationService _navigationService;

    public event PropertyChangedEventHandler PropertyChanged;

    public void OnPropertyChanged(string propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public MyViewModel(INavigationService navigationService)
    {
        _navigationService = navigationService;
    }

    private ICommand _doSomething;

    public ICommand DoSomething
    {
        get
        {
            return _doSomething ??
                new RelayCommand(() =>
                    {
                        _navigationService.Navigate(typeof(Page2));
                    });
        }
    }}

В простом примере Ive создала viewmodel в MainPage.cs и добавила NavigationService но вы можете сделать это в другом месте в зависимости от того, как выглядит ваша настройка MVVM.

MainPage.cs

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        this.InitializeComponent();

        var vm = new MyViewModel(new NavigationService());
        this.DataContext = vm;
    }
}

MainPage.xaml(привязывается к команде DoSomething)

 <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Button Width="200" Height="50" Content="Go to Page 2"
             Command="{Binding DoSomething}"/>
</Grid>

Надеюсь, что это поможет.

Ответ 3

Мне не очень нравится, когда ViewModel ссылается на Views для перехода к. Поэтому я предпочитаю первый подход ViewModel. Используя ContentControls, DataTemplates для типов ViewModel и некоторую модель навигации в моих моделях ViewModels.

Моя навигация выглядит следующим образом:

[ImplementPropertyChanged]
public class MainNavigatableViewModel : NavigatableViewModel
{
    public ICommand LoadProfileCommand { get; private set; }

    public ICommand OpenPostCommand { get; private set; }

    public MainNavigatableViewModel ()
    {
        LoadProfileCommand = new RelayCommand(() => Navigator.Navigate(new ProfileNavigatableViewModel()));
        OpenPostCommand = new RelayCommand(() => Navigator.Navigate(new PostEditViewModel { Post = SelectedPost }), () => SelectedPost != null);
    }
}

My NavigatableViewModel выглядит так:

[ImplementPropertyChanged]
public class NavigatableViewModel
{
    public NavigatorViewModel Navigator { get; set; }

    public NavigatableViewModel PreviousViewModel { get; set; }

    public NavigatableViewModel NextViewModel { get; set; }

}

И мой навигатор:

[ImplementPropertyChanged]
public class NavigatorViewModel
{
    public NavigatableViewModel CurrentViewModel { get; set; }

    public ICommand BackCommand { get; private set; }

    public ICommand ForwardCommand { get; private set; }

    public NavigatorViewModel()
    {
        BackCommand = new RelayCommand(() =>
        {
            // Set current control to previous control
            CurrentViewModel = CurrentViewModel.PreviousViewModel;
        }, () => CurrentViewModel != null && CurrentViewModel.PreviousViewModel != null);

        ForwardCommand = new RelayCommand(() =>
        {
            // Set current control to next control
            CurrentViewModel = CurrentViewModel.NextViewModel;
        }, () => CurrentViewModel != null && CurrentViewModel.NextViewModel != null);
    }

    public void Navigate(NavigatableViewModel newViewModel)
    {
        if (newViewModel.Navigator != null && newViewModel.Navigator != this)
            throw new Exception("Viewmodel can't be added to two different navigators");

        newViewModel.Navigator = this;

        if (CurrentViewModel != null)
        {
            CurrentViewModel.NextViewModel = newViewModel;
        }

        newViewModel.PreviousViewModel = CurrentViewModel;
        CurrentViewModel = newViewModel;
    }
}

My MainWindows.xaml:

<Window
        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:viewmodels="clr-namespace:MyApp.ViewModels"
        x:Class="MyApp.Windows.MainWindow"
        mc:Ignorable="d"
        Title="MainWindow" Height="389" Width="573" 
        d:DataContext="{d:DesignInstance {x:Type viewmodels:MyAppViewModel}, IsDesignTimeCreatable=True}">
    <Grid>
        <!-- Show data according to data templates as defined in App.xaml -->
        <ContentControl Content="{Binding Navigator.CurrentViewModel}"  Margin="0,32,0,0" />

        <Button Content="Previous" Command="{Binding Navigator.BackCommand}" Style="{DynamicResource ButtonStyle}" HorizontalAlignment="Left" Margin="10,5,0,0" VerticalAlignment="Top" Width="75" />
        <Button Content="Next" Command="{Binding Navigator.ForwardCommand}" Style="{DynamicResource ButtonStyle}" HorizontalAlignment="Left" Margin="90,5,0,0" VerticalAlignment="Top" Width="75" />
    </Grid>
</Window>

App.xaml.cs:

public partial class App
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        new MainWindow {DataContext = new MyAppViewModel()}.Show();
    }
}

MyAppViewModel:

[ImplementPropertyChanged]
public class MyAppViewModel
{
    public NavigatorViewModel Navigator { get; set; }

    public MyAppViewModel()
    {
        Navigator = new NavigatorViewModel();
        Navigator.Navigate(new MainNavigatableViewModel());
    }
}

App.xaml:

        <DataTemplate DataType="{x:Type viewmodels:MainNavigatableViewModel}">
            <controls:MainControl/>
        </DataTemplate>
        <DataTemplate DataType="{x:Type viewmodels:PostEditViewModel}">
            <controls:PostEditControl/>
        </DataTemplate>

Недостатком является то, что у вас есть больше ViewModel-кода, который управляет состоянием того, что вы смотрите. Но, очевидно, это также огромное преимущество в плане тестирования. И, конечно, ваши ViewModels не должны зависеть от ваших представлений.

Плюс я использую Fody/PropertyChanged, о котором идет речь [ImplementPropertyChanged]. Не позволяет мне писать код OnPropertyChanged.

Ответ 4

Вот еще один способ реализовать NavigationService без использования абстрактного класса и без ссылок на типы просмотров в вашей модели представления.

Предполагая, что модель представления целевой страницы выглядит примерно так:

public interface IDestinationViewModel { /* Interface of destination vm here */ }
class MyDestinationViewModel : IDestinationViewModel { /* Implementation of vm here */ }

Затем ваш NavigationService может просто реализовать следующий интерфейс:

public interface IPageNavigationService
{
    void NavigateToDestinationPage(IDestinationViewModel dataContext);
}

В главном окне ViewModel вам нужно ввести навигатор и модель представления целевой страницы:

class MyViewModel1 : IMyViewModel
{
    public MyViewModel1(IPageNavigationService navigator, IDestinationViewModel destination)
    {
        GoToPageCommand = new RelayCommand(() => 
                navigator.NavigateToDestinationPage(destination));
    }

    public ICommand GoToPageCommand { get; }
}

Реализация NavigationService инкапсулирует тип представления (Page2) и ссылку на кадр, который вводится через конструктор:

class PageNavigationService : IPageNavigationService
{
    private readonly Frame _navigationFrame;

    public PageNavigationService(Frame navigationFrame)
    {
        _navigationFrame = navigationFrame;
    }

    void Navigate(Type type, object dataContext)
    {
        _navigationFrame.Navigate(type);
        _navigationFrame.DataContext = dataContext;
    }

    public void NavigateToDestinationPage(IDestinationViewModel dataContext)
    {
        // Page2 is the corresponding view of the destination view model
        Navigate(typeof(Page2), dataContext);
    }
}

Чтобы получить фрейм, просто назовите его в своем MainPage xaml:

<Frame x:Name="RootFrame"/>

В коде позади MainPage инициализируйте свой загрузочный блок, передав корневой кадр:

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        this.InitializeComponent();
        var bootstrapper = new Bootstrapper(RootFrame);
        DataContext = bootstrapper.GetMainScreenViewModel();
    }
}

Наконец, для полноты реализации является реализация bootstrapper;)

class Bootstrapper
{
    private Container _container = new Container();

    public Bootstrapper(Frame frame)
    {
        _container.RegisterSingleton(frame);
        _container.RegisterSingleton<IPageNavigationService, PageNavigationService>();
        _container.Register<IMyViewModel, MyViewModel1>();
        _container.Register<IDestinationViewModel, IDestinationViewModel>();

#if DEBUG
        _container.Verify();
#endif
    }

    public IMyViewModel GetMainScreenViewModel()
    {
        return _container.GetInstance<IMyViewModel>();
    }
}