如何在WPF应用程序中的页面之间切换?
我试图创建一个WPF应用程序,该应用程序提供一个登录视图,并在成功登录后提供一个第一页,第二页和第三页(如向导).包括登录视图的每个页面"都有其各自的 ViewModel
.我有一个 MainWindow.xaml
,其中包含四个 UserControls
,其中一个在任何给定状态下都可见.
I am trying to create a WPF application that presents a login view and after successful login, presents a first, second, and third page (like a wizard). Each "page" including the login view has its respective ViewModel
. I have a MainWindow.xaml
which holds four UserControls
one of which will be visible at any given state.
我在处理能见度编排时遇到麻烦.对我来说,最有意义的是 MainWindowViewModel
是负责跟踪哪个 UserControl
是当前可见的那个,但我似乎不太了解代码起作用.
I am having trouble dealing with the orchestration of visibility. It makes the most sense to me that MainWindowViewModel
is the one that is responsible for keeping track of which UserControl
is the current visible one but I can't quite seem to get the code to work.
为了简化起见,我只会显示 MainWindow
和 LoginView
的相关文件.
I will only show the relevant files for the MainWindow
and the LoginView
to keep things simpler.
MainWindow.xaml
<Grid>
<local:LoginView Visibility="{Not sure what to bind to here}" />
<local:PageOne Visibility="{Not sure what to bind to here}" />
<local:PageTwo Visibility="{Not sure what to bind to here}" />
<local:PageThree Visibility="{Not sure what to bind to here}" />
</Grid>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}
}
MainWindowViewModel.cs
公共类MainWindowViewModel:BaseViewModel{公共ICommand WindowClosingCommand {get;}
public class MainWindowViewModel : BaseViewModel { public ICommand WindowClosingCommand { get; }
public MainWindowViewModel()
{
WindowClosingCommand = new WindowClosingCommand(this);
}
}
LoginView.xaml
<UserControl x:Class="MyProject.View.LoginView"
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:MyProject.View"
mc:Ignorable="d"
d:DesignHeight="800" d:DesignWidth="1200">
<Grid>
<!-- UI Layout stuff here -->
</Grid>
</UserControl>
LoginView.xaml.cs
public partial class Login : UserControl
{
public Login()
{
InitializeComponent();
DataContext = new LoginViewModel();
}
}
LoginViewModel.cs
public class LoginViewModel : BaseViewModel
{
public ICommand ConnectCommand { get; }
public ICommand WindowClosingCommand { get; }
public LoginViewModel()
{
ConnectCommand = new ConnectCommand(this);
WindowClosingCommand = new WindowClosingCommand(this);
}
public string UserName { get; set; }
}
如您所见,我想避免在文件 .xaml.cs
后面的代码中添加大量逻辑,因为这是最佳实践,并且我有一个 ViewModel
哪个 .xaml
文件.现在,通常,我会这样写:
So as you can see, I want to avoid putting a ton of logic in the code behind files .xaml.cs
because that is best practice and I have a ViewModel
for which .xaml
file. Now, ordinarily, I would write something like:
public PageType CurrentPage;
public enum PageType
{
Login, PageOne, PageTwo, PageThree
}
public Visibility LoginVisibility
{
get { (CurrentPage == PageType.Login) ? Visibility.Visible : Visibility.Collapsed }
}
// Repeat for each of the other three pages
然后根据是否在每个页面上单击下一步"或后退"按钮,我将正确设置 CurrentPage
字段.
And then depending on if the "Next" or "Back" buttons were clicked on each page, I would set the CurrentPage
field properly.
但是,如果我们回到我的 MainWindow.xaml
,我就不能这样做:
However, if we refer back to my MainWindow.xaml
, I can't just do:
<local:LoginView Visibility="{Binding LoginVisibility}" />
因为 LoginViewModel
中不存在 LoginVisibility
,因此该控件是用户控件的数据上下文.而且,将该字段放在此处并不恰当,因为所有 ViewModels
随后都需要知道自己的可见性状态,并以某种方式将其传达给 MainWindow
.
Because LoginVisibility
does not exist in the LoginViewModel
which is what that user control's data context is. And it wouldn't feel right to put that field in there because all ViewModels
will then need to know their own visibility state and somehow communicate that up to the MainWindow
.
基本上,我很困惑,不确定如何在应用程序中的页面之间进行切换.任何帮助或指导将不胜感激.
Basically, I am confused and unsure how to go about toggling between pages in my application. Any help or guidance would be greatly appreciated.
与使用 Frame
相对,最简单,最轻便的方法是为每个页面创建一个视图模型.然后创建一个包含所有页面并管理其选择的主视图模型. ContentControl
将使用分配给 ContentControl.ContentTemplate
属性的 DataTemplate
显示视图模型,或者在多页方案中使用 DataTemplateSelector
分配给 ContentControl.ContentTemplateSelector
或隐式模板,方法是仅定义 DataTemplate.DataType
而没有 Key
属性:
The easiest and most lightweight way opposed to using a Frame
, is to create a view model for each page. Then create a main view model which holds all pages and manages their selection. A ContentControl
will display the view models using a DataTemplate
assigned to the ContentControl.ContentTemplate
property or in a multi page scenario either a DataTemplateSelector
assigned to ContentControl.ContentTemplateSelector
or implicit templates by only defining the DataTemplate.DataType
without the Key
attribute:
MainWindow.xaml
<Window>
<Window.DataContext>
<MainViewModel x:Key="MainViewModel" />
</Window.DataContext>
<Window.Resources>
<!--
The templates for the view of each page model.
Can be moved to dedicated files.
-->
<DataTemplate DataType="{x:Type LoginViewModel}">
<Border Background="Coral">
<!-- UserControl -->
<local:LoginView />
</Border>
</DataTemplate>
<DataTemplate DataType="{x:Type PageOneViewModel}">
<Border Background="Red">
<local:PageOne />
</Border>
</DataTemplate>
<DataTemplate DataType="{x:Type PageTwoViewModel}">
<Border Background="DeepSkyBlue">
<TextBox Text="{Binding PageTitle}" />
</Border>
</DataTemplate>
</Window.Resources>
<StackPanel>
<Button Content="Load Login Page"
Command="{Binding SelectPageFromIndexCommand}"
CommandParameter="0" />
<Button Content="Load Page One"
Command="{Binding SelectPageFromIndexCommand}"
CommandParameter="1" />
<Button Content="Load Next Page"
Command="{Binding SelectNextPageCommand}" />
<!-- The actual page control -->
<ContentControl Content="{Binding SelectedPage}" />
</StackPanel>
</Window>
视图模型
MainViewModel.cs
class MainViewModel : INotifyPropertyChanged
{
public MainViewModel()
{
this.Pages = new ObservableCollection<IPageViewModel>()
{
new LoginViewModel(),
new PageOneViewModel(),
new PageTwoViewModel()
};
// Show startup page
this.SelectedPage = this.Pages.First();
}
// Define the Execute and CanExecute delegates for the command
// and pass them to the constructor
public ICommand SelectPageFromIndexCommand => new SelectPageCommand(
param => this.SelectedPage = this.Pages.ElementAt(int.Parse(param as string)),
param => int.TryParse(param as string, out int index));
// Define the Execute and CanExecute delegates for the command
// and pass them to the constructor
public ICommand SelectNextPageCommand => new SelectPageCommand(
param => this.SelectedPage = this.Pages.ElementAt(this.Pages.IndexOf(this.SelectedPage) + 1),
param => this.Pages.IndexOf(this.SelectedPage) + 1 < this.Pages.Count);
private IPageViewModel selectedPage;
public IPageViewModel SelectedPage
{
get => this.selectedPage;
set
{
if (object.Equals(value, this.selectedPage))
{
return;
}
this.selectedPage = value;
OnPropertyChanged();
}
}
private ObservableCollection<IPageViewModel> pages;
public ObservableCollection<IPageViewModel> Pages
{
get => this.pages;
set
{
if (object.Equals(value, this.pages))
{
return;
}
this.pages = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
SelectPageCommand.cs
class SelectPageCommand : ICommand
{
public SelectPageCommand(Action<object> executeDelegate, Predicate<object> canExecuteDelegate)
{
this.ExecuteDelegate = executeDelegate;
this.CanExecuteDelegate = canExecuteDelegate;
}
private Predicate<object> CanExecuteDelegate { get; }
private Action<object> ExecuteDelegate { get; }
#region Implementation of ICommand
public bool CanExecute(object parameter) => this.CanExecuteDelegate?.Invoke(parameter) ?? false;
public void Execute(object parameter) => this.ExecuteDelegate?.Invoke(parameter);
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
#endregion
}
页面模型
IPageViewModel.cs
// Base type for all pages
interface IPageViewModel : INotifyPropertyChanged
{
public string PageTitle { get; set; }
}
LoginViewModel.cs
// BaseViewModel implementation.
// Consider to introduce dedicated abstract class Page which implements IPageViewModel
class LoginViewModel : BaseViewModel
{
// Implementation
}
PageOneViewModel.cs
// BaseViewModel implementation.
// Consider to introduce dedicated abstract class Page which implements IPageViewModel
class PageOneViewModel : IPageViewModel
{
// Implementation
}
PageTwoViewModel.cs
// BaseViewModel implementation.
// Consider to introduce dedicated abstract class Page which implements IPageViewModel
class PageTwoViewModel : IPageViewModel
{
// Implementation
}