Monday, September 10, 2012

Simple navigation implementation in my first Windows 8 app

As mentioned in previous post, I'm working on my first Windows 8 app. In the upcoming beta version, I will introduce simple navigation function. The simple navigation is really simple, the app just provides 2 buttons, Previous and Next, to help the child browse a list of characters and numbers. It also means the Previous button should be disabled when the app is at the first character, and the Next button should be disabled when the app is at the last character. And I will use MVVM pattern (without any supporting from MVVM framework) to implement the function.

To learn how to implement it, I create a sample project instead of doing it directly in the app project. This sample project works the same way as my real project. It provides an UI with 2 buttons to move previous/next, one label to display the number and a list of number to navigate between. Let's start with the UI.
    
        
The most important parts are binding commands and the number to display from the view model.

As applying MVVM pattern, the code behind file for this UI is very simple.
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();

            DataContext = new MainPageViewModel();
        }

        /// 
        /// Invoked when this page is about to be displayed in a Frame.
        /// 
        /// Event data that describes how this page was reached.  The Parameter
        /// property is typically used to configure the page.
        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
        }
    }
Now are the commands. I create a abstract MoveCommand which implement ICommand to share CanExecuteChanged and RaiseCanExecuteChanged()
    public abstract class MoveCommand : ICommand
    {
        public abstract bool CanExecute(object parameter);
        public abstract void Execute(object parameter);

        public event EventHandler CanExecuteChanged;

        public void RaiseCanExecuteChanged()
        {
            if (CanExecuteChanged != null)
            {
                CanExecuteChanged(this, EventArgs.Empty);
            }
        }
    }
The MoveNextCommand and MovePreviousCommand extend MoveCommand and provide implementation for abstract methods.
    public class MoveNextCommand : MoveCommand
    {
        private readonly MainPageViewModel _viewModel;

        public MoveNextCommand(MainPageViewModel viewModel)
        {
            _viewModel = viewModel;
        }

        public override bool CanExecute(object parameter)
        {
            return _viewModel.CanMoveNext;
        }

        public override void Execute(object parameter)
        {
            _viewModel.MoveNext();

            RaiseCanExecuteChanged();
        }
    }

    public class MovePreviousCommand : MoveCommand
    {
        private readonly MainPageViewModel _viewModel;

        public MovePreviousCommand(MainPageViewModel viewModel)
        {
            _viewModel = viewModel;
        }

        public override bool CanExecute(object parameter)
        {
            return _viewModel.CanMovePrevious;
        }

        public override void Execute(object parameter)
        {
            _viewModel.MovePrevious();

            RaiseCanExecuteChanged();
        }
    }
The last thing is the view model class
    public class MainPageViewModel : INotifyPropertyChanged
    {
        private readonly List _numbers;
        private int _currentIndex;

        public int Number
        {
            get { return _numbers[_currentIndex]; }
        }

        public ICommand MovePreviousCommand { get; private set; }
        public ICommand MoveNextCommand { get; private set; }

        public MainPageViewModel()
        {
            _numbers = new List {1, 2, 3};

            _currentIndex = 0;

            MovePreviousCommand = new MovePreviousCommand(this);
            MoveNextCommand = new MoveNextCommand(this);
        }

        public bool CanMovePrevious
        {
            get { return _currentIndex > 0; }
        }

        public bool CanMoveNext
        {
            get { return _currentIndex < _numbers.Count - 1; }
        }

        public void MovePrevious()
        {
            if (CanMovePrevious)
            {
                _currentIndex--;

                PropertyChanged(this, new PropertyChangedEventArgs("Number"));
            }
        }

        public void MoveNext()
        {
            if (CanMoveNext)
            {
                _currentIndex++;

                PropertyChanged(this, new PropertyChangedEventArgs("Number"));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
When MoveNextCommand or MovePreviousCommand is fired, the appropriate method (MoveNext() or MovePrevious()) is called. The method checks if it's possible to move then increase or decrease the index and fire the ProperyChanged event to notify the UI to update the label's text.

Now the app seems looks fine, the buttons should work as expected. But no, there is a problem. When I click Next button, the label's text was updated but the Previous button is still disabled. According to the ICommand document, when the command is bind to the button, the button will call CanExecute() method to check whether the command should or should not be enabled. The Previous button is still disabled because we didn't fire its CanExecuteChanged event yet. To fire the CanExecuteChanged event, I need to update the code as below.
    public sealed partial class MainPage : Page
    {
        private readonly MainPageViewModel _viewModel;

        public MainPage()
        {
            this.InitializeComponent();

            _viewModel = new MainPageViewModel();
            _viewModel.MovePreviousCommand.CanExecuteChanged += MovePreviousCommand_CanExecuteChanged;
            _viewModel.MoveNextCommand.CanExecuteChanged += MoveNextCommand_CanExecuteChanged;

            DataContext = _viewModel;
        }

        void MoveNextCommand_CanExecuteChanged(object sender, EventArgs e)
        {
            if (((CancelEventArgs)e).Cancel)
            {
                return;
            }

            ((MoveCommand)_viewModel.MovePreviousCommand).RaiseCanExecuteChanged(true);
        }

        void MovePreviousCommand_CanExecuteChanged(object sender, EventArgs e)
        {
            if (((CancelEventArgs)e).Cancel)
            {
                return;
            }

            ((MoveCommand)_viewModel.MoveNextCommand).RaiseCanExecuteChanged(true);
        }

        /// 
        /// Invoked when this page is about to be displayed in a Frame.
        /// 
        /// Event data that describes how this page was reached.  The Parameter
        /// property is typically used to configure the page.
        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
        }
    }

    public abstract class MoveCommand : ICommand
    {
        public abstract bool CanExecute(object parameter);
        public abstract void Execute(object parameter);

        public event EventHandler CanExecuteChanged;

        public void RaiseCanExecuteChanged(bool cancel)
        {
            if (CanExecuteChanged != null)
            {
                CanExecuteChanged(this, new CancelEventArgs {Cancel = cancel});
            }
        }
    }

    public class MoveNextCommand : MoveCommand
    {
        private readonly MainPageViewModel _viewModel;

        public MoveNextCommand(MainPageViewModel viewModel)
        {
            _viewModel = viewModel;
        }

        public override bool CanExecute(object parameter)
        {
            return _viewModel.CanMoveNext;
        }

        public override void Execute(object parameter)
        {
            _viewModel.MoveNext();

            RaiseCanExecuteChanged(!_viewModel.CanMoveNext);
        }
    }

    public class MovePreviousCommand : MoveCommand
    {
        private readonly MainPageViewModel _viewModel;

        public MovePreviousCommand(MainPageViewModel viewModel)
        {
            _viewModel = viewModel;
        }

        public override bool CanExecute(object parameter)
        {
            return _viewModel.CanMovePrevious;
        }

        public override void Execute(object parameter)
        {
            _viewModel.MovePrevious();

            RaiseCanExecuteChanged(!_viewModel.CanMovePrevious);
        }
    }
Here I use CancelEventArgs instead of EventArgs to stop firing event at the right time. Otherwise it can lead to StackOverflowException.

Now the app works as I expected. When the app starts, the Previous button should be disabled, the Next button should be enabled. When I click Next, the label's text was updated and the Previous button should be enabled. When I move to last item, the Next button should be disabled while the Previous button should be enabled...

Even now the app works as I expected, I don't like this implementation when it's not pure MVVM, I still need to add code to code behind file to make the app works.

In the next post, I will update the code so it will "more MVVM". Stay tuned!

Source code for this post, you can get from here.

No comments:

Post a Comment