1

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.

noblerare
  • 10,277
  • 23
  • 78
  • 140
  • Use `ContentControl` with datatemplates for your VMs. Create property on your MainVM to store current VM. Once you bind the contentcontrol to current viewmodel there is no need for enum. And you don't need to handle visibility either. – XAMlMAX Nov 14 '19 at 08:51
  • Usually I would recommend viewmodel first. A wizard is one of the few requirements I would consider using pages in a frame. You can easily navigate forwards and backwards within a frame. Each page retains it's view state. Put a reference to the viewmodels in a di container or resources or a mediator object and make the pages get their datacontext out of that. If you need to reset then instantiate the pages again and replace those instances. – Andy Nov 14 '19 at 11:47
  • Or you could go viewmodel first. You have usercontrols. You have viewmodels. All you need to do is tell it which uc to use for which vm, https://social.technet.microsoft.com/wiki/contents/articles/52485.wpf-tips-and-tricks-using-contentcontrol-instead-of-frame-and-page-for-navigation.aspx – Andy Nov 14 '19 at 11:47

2 Answers2

2

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.

Lingam
  • 609
  • 5
  • 16
2

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 : 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
}
BionicCode
  • 1
  • 4
  • 28
  • 44
  • Thanks for your comment. I think I am wrapping my head around what you're trying to do here. However, how do the PageModels (e.g. `PageA.cs` and `PageB.cs`) relate to the UI in UserControls (`PageOne.xaml`) that I've designed? – noblerare Nov 14 '19 at 14:09
  • I adjusted the code examples. You can now copy and paste the code and it will run. Just add your implementations of the view models and add your view implementations so that the objects defined in the `DataTemplate` can be instantiated. The basic idea is that you wrap your page views into a `DataTemplate` which has it's `DataType` set to the model type that is the data source i.e. the `DataContext` of the wrapped view. I also added a `SelectNextPageCommand` and added a corresponding `Button` to the view to show the basic principle and its simplicity. You can now select by index and next page. – BionicCode Nov 14 '19 at 15:24
  • Just make sure all page models derive from a common type. Thia common type is the generic parameter for the collection that holds all pages. Also make sure that each implementation of the common base type has a dedicated `DataTemplate` defined within the scope of the `ContentControl` that binds to the page model collection to render the pages Otherwise the templates won't apply automatically. – BionicCode Nov 14 '19 at 15:47