C# WPF页面(视图)间导航

3
我将创建一个类和方法,可用于任何窗口和页面,以更改在MainWindow窗口上显示的当前页面。
到目前为止,我已经获得了以下内容:
class MainWindowNavigation : MainWindow
{
    public MainWindow mainWindow;

   public void ChangePage(Page page)
    {
        mainWindow.Content = page;
    }
}

主窗口本身:
public MainWindow()
    {
        InitializeComponent();
        MainWindowNavigation mainWindow = new MainWindowNavigation();
        mainWindow.ChangePage(new Pages.MainWindowPage());
    }

很不幸,这会导致System.StackOverflowException。
创建此内容是为了让我能够从当前显示在mainWindow.Content中的页面更改mainWindow.Content。
我已经检查过MVVM了,但我认为对于像这样的小应用程序来说没有必要使用它,因为我只想在打开时显示欢迎页面,然后在侧边栏上有几个按钮。一旦按下,mainWindow.Content就会正确地更改为一个页面,在该页面上用户可以输入登录详细信息,然后在登录页面上按下按钮后,如果成功验证了输入的登录详细信息,我希望更改mainWindow.Content到另一页。
3个回答

11

使用MVVM是完全可以的,因为它将简化您对需求的实现。WPF是设计用来配合MVVM模式的,这意味着要大量使用数据绑定和数据模板。

任务相当简单。为每个视图创建一个(或),例如和,以及它们对应的视图模型和。

将显示网页。
主要的技巧是,在使用隐式(没有定义的模板资源)时,XAML解析器会自动查找并应用正确的模板,其中匹配的当前内容类型。这使得导航非常简单,因为您只需要从页面模型集合中选择当前页面并将此页面通过数据绑定设置到或的属性中:

用法

MainWindow.xaml

<Window>
  <Window.DataContext>
    <MainViewModel />
  </Window.DataContext>

  <Window.Resources>
    <DataTemplate DataType="{x:Type WelcomePageviewModel}">
      <WelcomPage />
    </DataTemplate>

    <DataTemplate DataType="{x:Type LoginPageviewModel}">
      <LoginPage />
    </DataTemplate>
  </Window.Resources>

  <StackPanel>
  
    <!-- Page navigation -->
    <StackPanel Orientation="Horizontal">
      <Button Content="Show Login Screen" 
              Command="{Binding SelectPageCommand}" 
              CommandParameter="{x:Static PageName.LoginPage}" />
      <Button Content="Show Welcome Screen" 
              Command="{Binding SelectPageCommand}" 
              CommandParameter="{x:Static PageName.WelcomePage}" />
    </StackPanel>
  
    <!-- 
      Host of SelectedPage. 
      Automatically displays the DataTemplate that matches the current data type 
    -->
    <ContentControl Content="{Binding SelectedPage}" />
  <StackPanel>
</Window>

实现

  1. 创建单个页面控件(承载页面内容的控件)。可以使用 ControlUserControlPage 或者简单的 DataTemplate

WelcomePage.xaml

    <UserControl>
      <StackPanel>
        <TextBlock Text="{Binding PageTitle}" />
        <TextBlock Text="{Binding Message}" />
      </StackPanel>
    </UserControl>

LoginPage.xaml

    <UserControl>
      <StackPanel>
        <TextBlock Text="{Binding PageTitle}" />
        <TextBox Text="{Binding UserName}" />
      </StackPanel>
    </UserControl>
  1. 创建页面模型:

IPage.cs

    interface IPage : INotifyPropertyChanged
    {
      string PageTitel { get; set; }
    }

WelcomePageViewModel.cs

    class WelcomePageViewModel : IPage
    {
      private string pageTitle;   
      public string PageTitle
      {
        get => this.pageTitle;
        set 
        { 
          this.pageTitle = value; 
          OnPropertyChanged();
        }
      }

      private string message;   
      public string Message
      {
        get => this.message;
        set 
        { 
          this.message = value; 
          OnPropertyChanged();
        }
      }

      public WelcomePageViewModel()
      {
        this.PageTitle = "Welcome";
      }

      public event PropertyChangedEventHandler PropertyChanged;
      protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
      {
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
      }
    }

LoginPageViewModel.cs

    class LoginPageViewModel : IPage
    {
      private string pageTitle;   
      public string PageTitle
      {
        get => this.pageTitle;
        set 
        { 
          this.pageTitle = value; 
          OnPropertyChanged();
        }
      }

      private string userName;   
      public string UserName
      {
        get => this.userName;
        set 
        { 
          this.userName = value; 
          OnPropertyChanged();
        }
      }

      public LoginPageViewModel()
      {
        this.PageTitle = "Login";
      }

      public event PropertyChangedEventHandler PropertyChanged;
      protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
      {
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
      }
    }
  1. 创建页面标识符的枚举(用于消除XAML和C#中的魔法字符串):

PageName.cs

    public enum PageName
    {
      Undefined = 0, WelcomePage, LoginPage
    }
  1. 创建MainViewModel,用于管理页面和导航:

MainViewModel.cs
可以在Microsoft Docs:模式-WPF应用程序使用模型-视图-视图模型设计模式- 中找到RelayCommand的实现。

    class MainViewModel
    {
      public ICommand SelectPageCommand => new RelayCommand(SelectPage);

      private Dictionary<PageName, IPage> Pages { get; }

      private IPage selectedPage;   
      public IPage SelectedPage
      {
        get => this.selectedPage;
        set 
        { 
          this.selectedPage = value; 
          OnPropertyChanged();
        }
      }

      public MainViewModel()
      {
        this.Pages = new Dictionary<PageName, IPage>
        {
          { PageName.WelcomePage, new WelcomePageViewModel() },
          { PageName.LoginPage, new LoginPageViewModel() }
        };

        this.SelectedPage = this.Pages.First().Value;
      }

      public void SelectPage(object param)
      {
        if (param is PageName pageName 
          && this.Pages.TryGetValue(pageName, out IPage selectedPage))
        {
          this.SelectedPage = selectedPage;
        }
      }

      public event PropertyChangedEventHandler PropertyChanged;
      protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) 
        => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

4
每次我需要处理WPF相关的任务时,我不断地回到这里。这是一个非常好的起点教程。 - undefined
更换从欢迎界面到登录界面时,欢迎窗口会被销毁吗? - undefined
1
@Luiey 这取决于情况。页面的状态存储在页面模型中。在这个例子中,页面并没有被“销毁”,因为模型被存储在字典中。这样你就可以在视图之间前进和后退而不会丢失数据。如果你想要销毁一个视图,你必须删除对模型的每个引用,例如从字典中移除它或者用一个新的实例替换它。 - undefined
@BionicCode 我明白了。顺便说一下,解释得很好。我只是对将MVVM应用于全屏(自助)无人值守支付应用程序(如订购食物、支付账单)感到好奇,是否应该使用MVVM还是保持之前的页面导航方式。虽然这是一个需要做出的决定,但我真的很喜欢MVVM的工作原理,尤其是当你付出努力编写完整的MVVM模式时。以前做过MVVM,但大多数都是单页面的。 - undefined
@Luiey 没有理由反对MVVM。它有助于将数据与UI分离,并为您的应用程序提供一个完美地延伸到WPF框架的良好结构。任何结构都比没有结构好。如果您为应用程序编写单元测试,就无法摆脱MVVM或任何类似的模式。 - undefined

1

你可能想把 MainWindowNavigation 定义为一个静态类,并在其中定义一个方法,该方法只需简单地更改当前 MainWindowContent

static class MainWindowNavigation
{
    public static void ChangePage(Page page)
    {
        var mainWindow = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault();
        if (mainWindow != null)
            mainWindow.Content = page;
    }
}

你可以在任何类中调用该方法,而无需引用MainWindow

MainWindowNavigation.ChangePage(new Pages.MainWindowPage());

0
我找到了一种解决方案,可以在不将所有对象初始化存储在内存中的情况下实现简化导航。在我看来,这个解决方案可以帮助你节省MVVM模式,并减少内存开销。

MainWindow.xaml

<Window x:Class="TestWpf.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:TestWpf"
    mc:Ignorable="d" Title="MainWindow" Height="450" Width="800">
<Grid Background="Green">
    
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <StackPanel Orientation="Horizontal" Grid.Row="0">
        <Button Content="Go to login" Command="{Binding NavigateCmd}" CommandParameter="{x:Type local:LoginPage}"/>
    </StackPanel>
    <Frame Grid.Row="1" Content="{Binding SelectedContent, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
                    HorizontalAlignment="Center" Width="200" Height="200"/>
</Grid>

</Window>

MainWindowViewModel.cs

public partial class MainWindowViewModel : INotifyPropertyChanged 
{
private object? content;

public MainWindowViewModel()
{
    NavigateCmd = new RelayCommand<object>(Navigate);
}

public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName]string? name = null)
{
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

public object? SelectedContent { get => content; set { content = value; OnPropertyChanged(); } }

public RelayCommand<object> NavigateCmd { get; }
private void Navigate(object? param = null)
{
    if (param is Type type == false) return;
    SelectedContent = Activator.CreateInstance(type);
    ((FrameworkElement)SelectedContent).DataContext = this; //could be any data context;
}
}

LoginPage.xaml

<Page x:Class="TestWpf.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:TestWpf"
  mc:Ignorable="d" 
  d:DesignHeight="450" d:DesignWidth="800"
  Title="LoginPage">

<Grid Background="Red">
    
</Grid>

当点击"转到登录"按钮时,它将调用"NaviateCmd"命令,该命令将调用"Navigate"方法并传递"Page"的"Type",您希望在"MainWindow.xaml"中显示。然后,在"Navigate"方法内部,我们创建我们页面的实例,设置必要的数据绑定,并将其放置在"SelectedContent"属性中。
如果您想要导航到不同的页面,您可以在"Button"的"CommandParameter"属性中传递所需页面的不同"Type"。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接