Навигация по страницам с помощью 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>();
}
}