다음을 통해 공유


WPF Tips and Tricks: Using ContentControl instead of Frame and Page for Navigation

Introduction

It's quite common to see posters asking questions on Stack Overflow which indicate they are using Frames and Pages for navigation. This is not how most commercial teams handle navigation though.

What Should I Do then?

You should use a ContentControl instead of a Frame and UserControls instead of Pages.

UserControls are almost the same as Pages but more flexible.

ContentControls are much lighter weight than frames and more flexible.

A frame has it's journal which "helpfully" holds a reference to every content you put into it just in case the user wants to navigate back to it.  Even when you disable this effect the value of certain input fields is maintained in memory.  All this is rarely useful and even if you wanted to do this there are better ways to handle the requirement.

A frame also has navigation controls which you will probably want to hide.

All in all a Frame has a load of overhead you're unlikely to find useful.

What about that Navigate method then?

There's a pattern called MVVM which almost all WPF development teams use. It is very common to use an approach called viewmodel first for MVVM navigation. There are numerous resources on the web explain this in more detail so once you've read the next paragraph, it's an idea to google the subject and read more.

ViewModel First Navigation

It's usual to have a viewmodel which is the datacontext to a window.  Controls in that window will then inherit that datacontext for binding. 

That viewmodel implements INotifyPropertyChanged and exposes a public property. Call that ContentViewModelProperty for our purposes here ( but pick something better when you do this in your real code).  The Content dependency property of the contentcontrol is then bound to that viewmodel property. Each UserControl you want to switch out would then be associated with a specific ViewModel using DataType.  View1 would (say) be associated with type of View1ViewModel. View2 would have View2ViewModel. 

In order to navigate to View1 you set  ContentViewModelProperty to an instance of View1ViewModel. To navigate to View2 you set it to an instance of View1ViewModel.

These are then templated into views.

Here's some sample code to give you the flavour. 

I have two different views I want to navigate between. LoginUC and UserUC.

 Title="MainWindow" Height="350" Width="525">
 <Window.Resources>
 <DataTemplate DataType="{x:Type local:LoginViewModel}">
 <local:LoginUC/>
 </DataTemplate>
 <DataTemplate DataType="{x:Type local:UserViewModel}">
 <local:UserUC/>
 </DataTemplate>
 </Window.Resources>
 <Window.DataContext>
 <local:MainWindowViewModel/>
 </Window.DataContext>
 <Grid>
 <Grid.ColumnDefinitions>
 <ColumnDefinition Width="100"/>
 <ColumnDefinition Width="*"/>
 </Grid.ColumnDefinitions>
 <ItemsControl ItemsSource="{Binding NavigationViewModelTypes}">
 <ItemsControl.ItemTemplate>
 <DataTemplate>
 <Button Content="{Binding Name}"
 Command="{Binding DataContext.NavigateCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
 CommandParameter="{Binding VMType}"
 />
 </DataTemplate>
 </ItemsControl.ItemTemplate>
 </ItemsControl>
 <ContentControl Grid.Column="1"
 Content="{Binding CurrentViewModel}"
 />
 </Grid>
</Window>

the itemscontrol is binding to a collection of types. These correspond to the viewmodels.

The Datatemplates will template each out into a view whose datacontext will be that viewmodel.

Here's the MainWindowViewModel:

public class MainWindowViewModel : INotifyPropertyChanged
{
 public string MainWinVMString { get; set; } = "Hello from MainWindoViewModel";
 
 public ObservableCollection<TypeAndDisplay> NavigationViewModelTypes { get; set; } = new ObservableCollection<TypeAndDisplay>
 (
 new List<TypeAndDisplay>
 {
 new TypeAndDisplay{ Name="Log In", VMType= typeof(LoginViewModel) },
 new TypeAndDisplay{ Name="User", VMType= typeof(UserViewModel) }
 }
 );
 
 private object currentViewModel;
 
 public object CurrentViewModel
 {
 get { return currentViewModel; }
 set { currentViewModel = value; RaisePropertyChanged(); }
 }
 private RelayCommand<Type> navigateCommand;
 public RelayCommand<Type> NavigateCommand
 {
 get
 {
 return navigateCommand
 ?? (navigateCommand = new RelayCommand<Type>(
 vmType =>
 {
 CurrentViewModel = null;
 CurrentViewModel = Activator.CreateInstance(vmType);
 }));
 }
 }
 
 public event PropertyChangedEventHandler PropertyChanged;
 private void RaisePropertyChanged([CallerMemberName] String propertyName = "")
 {
 if (PropertyChanged != null)
 {
 PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
 }
 }
}

That navigate command creates an instance of a viewmodel using the type provided. 

There's a slight wrinkle here if you want to "reset" a view.

When the view is given the same type it already has then it will not re-template the controls out so there will be no noticeable difference. The current state of controls will remain constant including sliders, drop downs or whatever.  That is often not what you want.

Setting the property to null ensures you always get a new version of the usercontrol templated when you click an entry.