marko devcic

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

Beginner MVVM Navigation Part 3

Posted on 23 April 2014

In first two parts of this series (part 1, part 2) we had one HostViewModel that held on to its children and one that disposed them as soon as they got out of view. In this part we're going to use a combination of that two. We're going to build them using weak references and let the GC decide when and if should they be collected. Since the GC will be determining the life time of our child view models, we can't call Dispose on them.

Here's the complete code.

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

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

            var resolver = new ViewModelResolver(getter);
            _childrenMap.Add(typeof(T), resolver);
        }

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

            var resolver = _childrenMap[type];
            ViewModelBase viewModel = null;
            resolver.Reference.TryGetTarget(out viewModel);
            if (viewModel == null)
            {
                viewModel = resolver.Getter();
                resolver.Reference.SetTarget(viewModel);
            }
            return viewModel;
        }

        private class ViewModelResolver
        {
            public WeakReference<ViewModelBase> Reference { get; private set; }
            public Func<ViewModelBase> Getter { get; private set; }

            public ViewModelResolver(Func<ViewModelBase> getter)
            {
                Reference = new WeakReference<ViewModelBase>(null);
                Getter = getter;
            }
        }
    }

And the main view model implementing it.

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()
        {
            _timer.Dispose();
            base.OnDispose();
        }
    }

And that's it, three ways how you can implement the MVVM navigation between view models. Each way has it's advantages and disadvantages, so use whatever suits your design best.

In one of the next posts I'll show you how you can easily add animation to view changes.