marko devcic

  • github:
    deva666
  • email:
    madevcic {at} gmail.com

Beginner MVVM Navigation Part 1

Posted on 1 March 2014

I'm going to discuss 3 ways how you can work with multiple view models hosted inside one main view model.

Now your first thought might be what's the big deal with navigation anyway, WPF has a Frame class, I'll just drop one inside my Main window, wire up the event handlers and change the Frame's source in the code behind. Yes that could definitely work, but it wouldn't be in a MVVM style.

So, we are going to build a sample book and authors catalog app (you can download the complete Visual studio project at the bottom of this post, link with source code is also at the bottom), just to have something we can switch between. The app will have a Main view model that acts as a host to other child view models and supplies the main window with some properties that are common to all other view models.

For hosting the child view models view, instead of Frame we're going to use ContentControl inside our Main window. Here's the complete code of our Main window UI.

<Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal">
            <Label Content="Books"
                    Margin="20, 10, 10,5"
                   Style="{StaticResource SelectableLabelStyle}">
                <Label.InputBindings>
                    <MouseBinding Gesture="LeftClick" 
                                  Command="{Binding GoToBooksCommand}"
                                  CommandParameter="Books"/>
                </Label.InputBindings>
            </Label>
            <Label Content="Authors"
                    Margin="10, 10, 10,5"
                   Style="{StaticResource SelectableLabelStyle}">
                <Label.InputBindings>
                    <MouseBinding Gesture="LeftClick"
                                  Command="{Binding GoToAuthorsCommand}"
                                  CommandParameter="Authors" />
                </Label.InputBindings>
            </Label>
            <Label Content="Settings"
                   Margin="10, 10, 10,5"
                   Style="{StaticResource SelectableLabelStyle}">
                <Label.InputBindings>
                    <MouseBinding Gesture="LeftClick"
                                  Command="{Binding GoToSettingsCommand}"
                                  CommandParameter="Settings" />
                </Label.InputBindings>
            </Label>
        </StackPanel>
        <Label Grid.Row="1"
               Content="{Binding SelectedChild.Title}"
               HorizontalAlignment="Center"
               FontSize="15"
               Margin="0" />
        <controls:FadingContentControl Grid.Row="2"
                                        Content="{Binding SelectedChild}" 
                                       />
        <StackPanel Orientation="Horizontal"
                    HorizontalAlignment="Right"
                    Grid.Row="3">
            <Label Content="{Binding Version}" 
                   ContentStringFormat="v{0}"
                   Margin="5"
                   />
            <Label Content="{Binding Time}" 
                   Margin="50,5,10,5"
                   HorizontalAlignment="Right"/>
        </StackPanel>
    </Grid>

Nothing special here, just notice that we are binding the ContentControl's Content dependency property to SelectedChild property of the HostViewModel. Also there are 2 labels (used as buttons) for invoking the ChangePageCommand. If you would run the code now the ContentControl would display the name of the SelectedChild view model. We are binding to the actual view model and the WPF doesn't know what to display , so it just calls the object's ToString method and renders it inside a TextBlock.

We'll fix that by adding two UserControls that are going to display actual data (code not shown here). Now we just need to tell WPF what view to display when the view model changes. So we'll add a DataTemplate for each view model we are hosting and a corresponding view inside it. Note that we'll be adding the DataTemplates as resources inside a resource dictionary and we're not adding a Key to them.

<DataTemplate DataType="{x:Type viewModels:BooksViewModel}">
       <views:BooksView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:AuthorsViewModel}">
    <views:AuthorsView />
</DataTemplate>

We're all set, now let's take a look how we're going to handle the HostViewModel implementation.

In this part, we're going to build the Host so that it doesn't hold on to other view models, i.e. whenever a user navigates to a new view old child view model get's disposed and we construct a new one. This implementation can be good if there's not much navigation expected and your child view models hold on to resources that need to be released as soon as the view switches. Otherwise, implementations from part 2 or part 3 might suit you better.

First, here's a complete code for this HostViewModel.

  internal abstract class HostViewModel : ViewModelBase
    {
        private readonly Dictionary<Type, Func<ViewModelBase>> _childrenMap = new Dictionary<Type, Func<ViewModelBase>>();

        private ViewModelBase _selectedChild;


        public ViewModelBase SelectedChild
        {
            get { return _selectedChild; }
            set
            {
                if (_selectedChild != null && _selectedChild.Title == value.Title)
                    return;

                SetPropertyValue(ref _selectedChild, value);
            }
        }

        protected void RegisterChild<T>(Func<T> getter) where T : ViewModelBase
        {
            Contract.Requires(getter != null);

            if (_childrenMap.ContainsKey(typeof(T)))
                return;

            _childrenMap.Add(typeof(T), getter);
        }

        protected ViewModelBase GetChild(Type type)
        {
            Contract.Requires(type != null);
            if (_childrenMap.ContainsKey(type) == false)
                throw new InvalidOperationException("Can't resolve type " + type.ToString());

            var viewModel = _childrenMap[type];
            return viewModel();
        }
    }

And the actual MainViewModel implementation

      internal sealed class MainViewModel : HostViewModel
    {
        private readonly Timer _timer;

        public override string Title { get { return "Main"; } }

        public string Version
        {
            get { return this.GetType().Assembly.GetName().Version.ToString(); }
        }

        private string _time;
        public string Time
        {
            get { return _time; }
            set { SetPropertyValue(ref _time, value); }
        }

        private ICommand _goToBooksCommand;
        public ICommand GoToBooksCommand
        {
            get { return _goToBooksCommand ?? (_goToBooksCommand = new DelegateCommand(GoToBooks)); }
        }

        private ICommand _goToAuthorsCommand;
        public ICommand GoToAuthorsCommand
        {
            get { return _goToAuthorsCommand ?? (_goToAuthorsCommand = new DelegateCommand(GoToAuthors)); }
        }

        private ICommand _goToSettingsCommand;
        public ICommand GoToSettingsCommand
        {
            get { return _goToSettingsCommand ?? (_goToSettingsCommand = new DelegateCommand(GoToSettings)); }
        }

        public MainViewModel()
        {
            this.RegisterChild<BooksViewModel>(() => new BooksViewModel());
            this.RegisterChild<AuthorsViewModel>(() => new AuthorsViewModel());
            this.RegisterChild<SettingsViewModel>(() => new SettingsViewModel());

            this.SelectedChild = GetChild(typeof(BooksViewModel));

            _timer = new Timer((s) => Time = DateTime.Now.ToLongTimeString(), this, 500, 500);
        }

        private void GoToBooks()
        {
            this.SelectedChild = GetChild(typeof(BooksViewModel));
        }

        private void GoToAuthors()
        {
            this.SelectedChild = GetChild(typeof(AuthorsViewModel));
        }

        private void GoToSettings()
        {
            this.SelectedChild = GetChild(typeof(SettingsViewModel));
        }

        protected override void OnDispose()
        {
            if (this.SelectedChild != null)
                ((ViewModelBase)SelectedChild).Dispose();
            _timer.Dispose();
            base.OnDispose();
        }
    }

Code is pretty much straight forward, clicking on a label invokes the command which changes the current child view model.

Again, frequent view model changing will end up with a lot of heap allocations, forcing the GC and hurting performance. So use when this type of behavior is needed.

And that's it, in the next post we're going to implement the HostViewModel so it holds on to its child view models, i.e. they live for the entire duration of the HostViewModel itself.

Download the project here.

Browse the source.

part 2, part 3