【问题标题】:How do I toggle between pages in a WPF application?如何在 WPF 应用程序中的页面之间切换?
【发布时间】:2019-11-14 02:32:50
【问题描述】:

我正在尝试创建一个 WPF 应用程序,该应用程序显示登录视图,并在成功登录后显示第一、第二和第三页(如向导)。包括登录视图的每个“页面”都有其各自的ViewModel。我有一个MainWindow.xaml,其中包含四个UserControls,其中一个在任何给定状态下都可见。

我在处理可见性编排方面遇到了麻烦。对我来说最有意义的是 MainWindowViewModel 负责跟踪哪个 UserControl 是当前可见的,但我似乎无法让代码正常工作。

为了简单起见,我将只显示MainWindowLoginView 的相关文件。

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 { 获取; }

    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 文件。现在,通常情况下,我会写如下内容:

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 字段。

但是,如果我们再参考我的MainWindow.xaml,我就不能这样做了:

<local:LoginView Visibility="{Binding LoginVisibility}" />

因为LoginVisibility 不存在于LoginViewModel 中,而LoginViewModel 正是该用户控件的数据上下文。把那个字段放在那里感觉不对,因为所有ViewModels 都需要知道他们自己的可见性状态,并以某种方式将其传达给MainWindow

基本上,我很困惑,不确定如何在我的应用程序的页面之间切换。任何帮助或指导将不胜感激。

【问题讨论】:

  • ContentControl 与您的 VM 的数据模板一起使用。在 MainVM 上创建属性以存储当前 VM。一旦将内容控件绑定到当前视图模型,就不需要枚举。而且您也不需要处理可见性。
  • 通常我会先推荐viewmodel。向导是我考虑在框架中使用页面的少数要求之一。您可以轻松地在框架内向前和向后导航。每个页面都保留它的视图状态。将对视图模型的引用放在 di 容器或资源或中介对象中,并使页面从中获取其数据上下文。如果您需要重置,请再次实例化页面并替换这些实例。
  • 或者你可以先去viewmodel。你有用户控件。你有视图模型。您需要做的就是告诉它哪个 uc 用于哪个 vm,social.technet.microsoft.com/wiki/contents/articles/…

标签: c# wpf xaml


【解决方案1】:

与使用Frame 相比,最简单、最轻量的方法是为每个页面创建一个视图模型。然后创建一个包含所有页面并管理它们的选择的主视图模型。 ContentControl 将使用分配给ContentControl.ContentTemplate 属性的DataTemplate 或在多页面场景中显示视图模型,或者将DataTemplateSelector 分配给ContentControl.ContentTemplateSelector 或通过仅定义不带@ 的DataTemplate.DataType 隐式模板987654328@属性:

视图

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 : IPageViewModel 
{
  // 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
}

【讨论】:

  • 感谢您的评论。我想我正在围绕你在这里尝试做的事情。但是,PageModel(例如 PageA.csPageB.cs)与我设计的 UserControls(PageOne.xaml)中的 UI 有什么关系?
  • 我调整了代码示例。您现在可以复制并粘贴代码,它将运行。只需添加视图模型的实现并添加视图实现,以便可以实例化DataTemplate 中定义的对象。基本思想是将页面视图包装到 DataTemplate 中,其中 DataType 设置为作为数据源的模型类型,即包装视图的 DataContext。我还添加了SelectNextPageCommand 并在视图中添加了相应的Button 以显示基本原理及其简单性。您现在可以按索引和下一页进行选择。
  • 只要确保所有页面模型都派生自一个通用类型。 Thia 通用类型是包含所有页面的集合的通用参数。还要确保公共基类型的每个实现都有一个在ContentControl 范围内定义的专用DataTemplate,该ContentControl 绑定到页面模型集合以呈现页面,否则模板将不会自动应用。
【解决方案2】:

您可以在主窗口资源中创建数据模板,而不是绑定可见性,并根据枚举更改将适当的数据模板绑定到控件模板(在网格内,您希望显示它的位置)

下面是一个粗略的想法。

在你的 mainwindow.xaml 中

 <Window.Resources>
    <ResourceDictionary>
        <DataTemplate x:Key="DTLoginView">
            <local:LoginView />
        </DataTemplate>
        <DataTemplate x:Key="DTPageOne">
            <local:PageOne />
        </DataTemplate>
    </ResourceDictionary>
</Window.Resources>

现在,在您的主窗口视图模型中,执行一些逻辑并基于它存储页面的值。您当前的页面属性应该实现 INotifyPropertyChanged,它应该如下所示。 (注意:我添加了 Haley.Flipper.MVVM nuget 包用于基本 MVVM 接线(免责声明:Haley nuget 包是我开发的)。您可以实现自己的 INotifyPropertyChanged 或使用一些 MVVM 库

  private PageType _CurrentPage;
    public PageType CurrentPage
    {
        get { return _CurrentPage; }
        set { _CurrentPage = value; onPropertyChanged(); }
    }

在 MainWindow 的 XAML 中。 (你有网格的地方)

<Grid x:Name="grdPages" DataContext={Binding}>
<ContentControl >
        <ContentControl.Style>
            <Style TargetType="{x:Type ContentControl}">
                <Style.Triggers>
                    <DataTrigger Binding="{Binding ElementName=grdPages, Path=DataContext.CurrentPage, NotifyOnSourceUpdated=True, UpdateSourceTrigger=PropertyChanged}" Value="0">
                        <Setter Property="ContentTemplate" Value="{StaticResource DTLoginView}"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding ElementName=grdPages, Path=DataContext.CurrentPage, NotifyOnSourceUpdated=True, UpdateSourceTrigger=PropertyChanged}" Value="1">
                        <Setter Property="ContentTemplate" Value="{StaticResource DTPageOne}"/>
                    </DataTrigger>
               </Style.Triggers>
            </Style>
        </ContentControl.Style>                
    </ContentControl>

如果您查看上面的 xaml 代码,我将 datatrigger 绑定的值设为“0”“1”,因为枚举应该有 0、1、2、3 等等。但是,您也可以直接将枚举绑定为值。进行一些搜索,您可以轻松找到答案。

当前页面的属性(枚举值)应该由某种逻辑(由您实现)设置。完成后,它会自动触发到 xaml 的通知。

希望这能以某种方式帮助您。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-03-30
    • 2016-02-01
    • 1970-01-01
    • 2019-04-07
    • 2016-05-17
    • 2017-12-08
    • 1970-01-01
    相关资源
    最近更新 更多