简体   繁体   中英

How do I toggle between pages in a WPF application?

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.

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.

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

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; }
}

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

And then depending on if the "Next" or "Back" buttons were clicked on each page, I would set the CurrentPage field properly.

However, if we refer back to my MainWindow.xaml , I can't just do:

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

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.

Instead of binding visibility, you can create data template in the main window resources and bind the appropriate data template to the control template (inside the grid, where you wish to display it) based on the enum changes

A rough idea below.

Inside your mainwindow.xaml

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

Now, inside you mainwindow viewmodel, do some logic and based on it, store the values for the page. Your current page property should implement INotifyPropertyChanged which should look something like below. ( Note: I have added Haley.Flipper.MVVM nuget package for basic MVVM wiring (Disclaimer: The Haley nuget package is developed by me). You can implement your own INotifyPropertyChanged or use some MVVM libraries )

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

Inside your XAML for MainWindow. (Where you have the grid)

<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>

If you look at the xaml code above, I have the value as "0" "1" for the datatrigger binding because enums should have 0,1,2,3 and so on. However, you can also directly bind the enum as the values. Do some search and you can easily find answer for that.

The property (enum value) for the current page should be set by some logic (implemented by you). Once that is done, it will automatically trigger the notification to the xaml.

Hope this could help you somehow.

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:

The View

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>

The View Model

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
}

The Page Models

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
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM