Windows Phone——MVVM模式下頁面導航及導航事件處理


問題

在使用MVVM模式(使用了MvvmLight框架)開發Windows Phone應用的時候,遇到如下問題:

  1. ViewModel中如何實現導航?
  2. ViewModel中如何處理OnNavigatedTo事件?
  3. ViewModel中如何處理所有界面導航事件(OnNavigatedTo、OnNavigatingFrom及OnNavigatedFrom)?

這里將第2和第3個問題分開,是因為如果只需處理OnNavigatedTo事件,有一個不得不提十分巧妙的解決辦法,后文具體展開。

ViewModel中實現導航

在ViewModel中實現導航有多種辦法,google一下就能搜到很多種做法,最簡單的莫過於定義一個Navigator類,在類中獲取PhoneApplicationFrame進行導航:

 public class Navigator
    {
        public Navigator()
        {
            _frame = ((App)App.Current).RootFrame;
        }

        public void Navigate(string uriString)
        {
            _frame.Navigate(new Uri(uriString, UriKind.Relative));
        }

        public void GoBack()
        {
            if (_frame.CanGoBack)
            {
                _frame.GoBack();
            }
        }

        private PhoneApplicationFrame _frame;
    }

然后在App.cs中定義一個靜態Navigator類型的屬性即可實現全局的調用。

public partial class App : Application
    {
        private static Navigator _navigator = null;
        public static Navigator Navigator
        {
            get
            {
                return _navigator ?? (_navigator = new Navigator());
            }
        }

        ...
    }

如果你也一樣使用了MvvmLight框架,可以參考Laurent Bugnion (GalaSoft) 的這篇博文《Navigation in a #WP7 application with MVVM Light》。

想要實現基本的導航功能相對來說比較簡單,核心就是使用PhoneApplicationFrame。

ViewModel中處理OnNavigatedTo事件

關於如何在ViewModel中實現導航以及處理OnNavigatedTo事件,目前個人覺得實現的最為巧妙的就是 Agile.Zhou(kklldog) 在博文《豆瓣電台WP7客戶端 MVVM重構記錄之使用MVVM Light的Message實現導航》中介紹的使用一個NavgationController對象來統一實現導航的方法。這里簡單描述一下原理及用法。

原文用一句話介紹了思路,這里引用一下:

當一個VM需要導航的時候,Send一個Message把導航的URL傳遞出去,這個消息被一個NavgationController截獲,執行導航操作,導航完成之后NavgationController會Send一個Message,通知導航到的View對應的ViewModel執行Navigated方法。

具體步驟是:

  1. 定義NavigationHelper類,實現真正的導航功能以及導航消息的發送。
    NavigationHelper類的GetPhoneFrameRoot方法其實是實現了PhoneApplicationFrame的單例模式,通過注冊PhoneApplicationFrameNavigated事件,達到了截獲OnNavigatedTo事件的目的,同時在事件方法中發送導航完成的消息。
    private static void GetPhoneFrameRoot()
    {
        ......
        _root = Application.Current.RootVisual as PhoneApplicationFrame;
        ......
    
        _root.Navigated += new System.Windows.Navigation.NavigatedEventHandler(RootNavigated);
    
    }
    
    private static void RootNavigated(object sender, System.Windows.Navigation.NavigationEventArgs e)
    {
        string token = e.Uri.OriginalString;
        if (token.Contains("?"))
        {
            int index = e.Uri.OriginalString.IndexOf('?');
            token = token.Substring(0, index);
        }
        Messenger.Default.Send(e.Uri,token);// 發送導航完成的消息
    }

    NavigationMsgSend方法實現發送導航的消息
    /// <summary>
    /// 發送導航Msg
    /// </summary>
    /// <param name="pageUri"></param>
    public  static void NavigationMsgSend(Uri pageUri)
    {
        Messenger.Default.Send(pageUri, MsgToken.Navigation);
    }
    /// <summary>
    /// 發送導航Msg
    /// </summary>
    /// <param name="pageUrl"></param>
    public static void NavigationMsgSend(string pageUrl)
    {
        Messenger.Default.Send(CreateUri(pageUrl), MsgToken.Navigation);
    }

    NavigatedMsgReg方法實現接收導航完成的消息
    /// <summary>
    /// 注冊導航完成MSG
    /// </summary>
    public static void NavigatedMsgReg(object recipient)
    {
        INavigation navigation = recipient as INavigation;
        if (navigation!=null)
        {
            Messenger.Default.Register<Uri>(recipient, navigation.GetViewUrl(), navigation.Navigated);// 調用實現INavigation接口的ViewModel的Navigated方法
        }
    }
  2. 定義INavigation接口,來約束ViewModel實現Navigated行為。
    public interface INavigation
    {
            /// <summary>
            /// 獲取對應的View的Url
            /// </summary>
            /// <returns></returns>
            string GetViewUrl();
            /// <summary>
            /// 導航完成后發生
            /// </summary>
            /// <param name="uri"></param>
            void Navigated(Uri uri);
    }
  3. 定義NavigationController,接收導航消息,實現真正的導航動作。
        public class NavigationController
        {
            public NavigationController()
            {
                Messenger.Default.Register<Uri>(this, MsgToken.Navigation, Navigation);
            }
    
            private void Navigation(Uri uri)
            {
                NavigationHelper.NavigationTo(uri);
            }
        }

具體用法詳見源碼,下載地址:http://dbfm7.codeplex.com/

我在使用這種做法的時候,發現每次在ViewModel的構造函數中都要通過調用NavigationHelper的NavigatedMsgReg方法來注冊接收導航完成的消息,這實在是繁瑣,既然每次都要注冊,能不能實現“自動”注冊呢?

我們都知道MVVM模式同MVC相同,提倡“約定大於配置”的思想,即把所有界面放在Views文件夾,頁面的路徑通常就是/Views/xxxx.xaml,那么就可以通過獲取頁面的強類型來得到頁面的名稱,從而到頁面的路徑。

具體做法是,修改INavigation接口為NavigationViewModelBase基類,該基類直接繼承MvvmLight的ViewModelBase

    public abstract class NavigationViewModelBase : ViewModelBase
    {        
        /// <summary>
        /// 注冊導航
        /// 約定頁面路徑為/Views/...
        /// </summary>
        /// <param name="page">頁面類型</param>
        protected NavigationViewModelBase(Type page)
        {
            var urlToken = string.Format("/Views/{0}.xaml", page.ToString().Split('.').LastOrDefault());
            Messenger.Default.Register<Uri>(this, urlToken, Navigationed);
        }

        /// <summary>
        /// 導航完成回調函數
        /// </summary>
        /// <param name="url"></param>
        protected abstract void Navigationed(Uri url);
    }

ViewModel則這樣寫:

    public class MainViewModel : NavigationViewModelBase
    {
        public MainViewModel()
            : base(typeof(MainPage))
        {
            ...
        }
		
	...		
		
        protected override void Navigationed(Uri url)
        {
		...
        }
    }

ViewModel中處理所有界面導航事件

在Windows Phone開發中,除非項目實在非常簡單,我們基本無法回避要使用頁面的OnNavigatedTo、OnNavigatingFrom、OnNavigatedFrom事件,來處理一下進入或離開頁面的邏輯,那么如何在ViewModel中直接使用這些事件呢?

我在stackoverflow見到通過在頁面的相關事件中發送消息到ViewModel的方式來實現在ViewModel處理這些事件(Handling the OnNavigatedFrom / OnNavigatedTo events in the ViewModel),這種做法雖然簡單直接,但在每個頁面的Code-behind里都這么發消息總感覺十分的蹩腳。

那么使用一個繼承自PhoneApplicationPage的頁面基類來統一做這些事情是個不錯的選擇。順着這個思路,一番google之后,在github的源碼庫里看到名為JoshClose已經這么做了,其實現簡單明了,我根據需要修改如下:

定義頁面基類PhoneApplicationPageBase

PhoneApplicationPageBase繼承自PhoneApplicationPage,作為所有頁面的基類,通過DataContext來調用自定義的NavigationViewModelBase基類中已定義的相關方法,實現ViewModel與頁面的解耦。

    public class PhoneApplicationPageBase : PhoneApplicationPage
    {
        protected PhoneApplicationPageBase()
        {
            Loaded += PageBaseLoaded;
        }

        private void PageBaseLoaded(object sender, RoutedEventArgs e)
        {
            var viewModel = DataContext as NavigationViewModelBase;
            if (viewModel != null)
            {
                viewModel.NavigationService = NavigationService;
            }
        }

        protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);

            var viewModel = DataContext as NavigationViewModelBase;
            if (viewModel != null)
            {
                viewModel.NavigationContext = NavigationContext;
                viewModel.OnNavigatedTo(e);
            }
        }

        protected override void OnNavigatingFrom(System.Windows.Navigation.NavigatingCancelEventArgs e)
        {
            base.OnNavigatingFrom(e);
            var viewModel = DataContext as NavigationViewModelBase;
            if (viewModel != null)
            {
                viewModel.NavigationContext = NavigationContext;
                viewModel.OnNavigatingFrom(e);
            }
        }

        protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)
        {
            base.OnNavigatedFrom(e);
            var viewModel = DataContext as NavigationViewModelBase;
            if (viewModel != null)
            {
                viewModel.NavigationContext = NavigationContext;
                viewModel.OnNavigatedFrom(e);
            }
        }
    }

定義ViewModel基類NavigationViewModelBase

通過定義ViewModel的基類,來實現行為約束和基本的實現,同時也可以直接獲取並使用NavigationService和NavigationContext對象。

    public abstract class NavigationViewModelBase : ViewModelBase
    {
        protected bool RemoveBackEntry { get; set; }

        public NavigationService NavigationService { get; set; }

        public NavigationContext NavigationContext { get; set; }

        public virtual void OnNavigatedTo(NavigationEventArgs e) { }

        public virtual void OnNavigatingFrom(NavigatingCancelEventArgs e) { }

        public virtual void OnNavigatedFrom(NavigationEventArgs e)
        {
            if (RemoveBackEntry)
            {
                RemoveBackEntry = false;
                NavigationService.RemoveBackEntry();
            }
        }
    }

具體用法

  1. 修改頁面的XAML,使之繼承自PhoneApplicationPageBase,同時修改.cs文件;
    xaml
    cs
  2. 所有作為頁面DataContext的ViewModel繼承基類NavigationViewModelBase;
    viewmodel
  3. 在ViewModel重寫基類虛方法,實現相關處理邏輯。
    override

第三種解決方案Demo:

download

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM