原文: MVVMLight 實作指定Frame控件的導航 在UWP開發中,利用漢堡菜單實作導航是常見的方法。漢堡菜單導航一般都需要建立一個Frame控件,并對其進行導航,但是在MvvmLight架構預設的NavigationService中,隻能對根Frame進行導航,這就需要我們實作自己的NavigationService了。
MvvmLight源碼簡析
- GalaSoft.MvvmLight.Views命名空間下的NavigationService繼承了同命名空間下的INavigationService接口但是并沒有實作。
namespace GalaSoft.MvvmLight.Views
{
public class NavigationService : INavigationService
{
public const string RootPageKey = "-- ROOT --";
public const string UnknownPageKey = "-- UNKNOWN --";
public NavigationService();
public string CurrentPageKey { get; }
public void Configure(string key, Type pageType);
public void GoBack();
public void NavigateTo(string pageKey);
public virtual void NavigateTo(string pageKey, object parameter);
}
}
- 具體的功能實作是在GalaSoft.MvvmLight.Platform命名空間下的NavigationService中,根據平台的不同有不同的實作。
- 我們要做的就是自己實作INavigationService并自己定義導航的Frame控件
實作自己的NavigationService
很奇怪,官方公布源碼的那個網站上沒有找到Win10平台的Platform源碼,是以我就把Win8.1的NavigationService實作複制下來,經測試能正常用。
- 在public virtual void NavigateTo(string pageKey, object parameter)方法下,我們可以看到這樣一行代碼:
這個語句就定義了用來導航的Frame控件(不止這個方法中有,另外2個方法中也有類似的語句)。var frame = ((Frame)Window.Current.Content);
- 我們把這幾個地方的frame變量值設定成自己的Frame控件就可以了。
- 我的修改方法:
- 我的Frame控件是放在MainPage中的,主要是為了實作漢堡菜單導航,我想大部分有這種需求的人都是為了實作類似的導航吧。
- 首先在MainPage.xaml.cs裡面加入一個MainPage類型的靜态Public變量
- 然後再構造函數中給變量指派為this
- 還要給自己的Frame控件寫一個屬性來擷取它
public static MainPage MPage; // 1. public Frame MyFrame // 3. { get { return ContentFrame; //ContentFrame是xaml中定義的Frame控件的名字 } } public MainPage() { InitializeComponent(); SystemNavigationManager.GetForCurrentView().BackRequested += SystemNavigationManagerBackRequested; Loaded += (s, e) => { Vm.RunClock(); }; MPage = this; // 2. }
- 然後就可以在自己的NavigationService裡面使用自己的Frame了。
這樣我們就把NavigationService的導航Frame設定成了MainPage裡面的ContentFrame控件。//var frame = ((Frame)Window.Current.Content); // 之前的語句 var frame = MainPage.MPage.MyFrame; // 自己寫的新語句
- 我的Frame控件是放在MainPage中的,主要是為了實作漢堡菜單導航,我想大部分有這種需求的人都是為了實作類似的導航吧。
- 不過我這個方法還不是最優的,因為這樣寫就把MainPage和NavigationService直接聯系起來了,增加了耦合性。我看到WPF中可以通過下面這個方法來擷取指定名字的Frame控件,不過在UWP裡面沒有這個方法。
var frame = GetDescendantFromName(Application.Current.MainWindow, "MainFrame") as Frame;
如果誰有更好的辦法的話麻煩在評論裡面告訴我,萬分感謝。
效果圖

MyNavigationService代碼
using GalaSoft.MvvmLight.Views;
using Mvvm_HutHelper.View;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Mvvm_HutHelper.Model
{
class MyNavigationService : INavigationService
{
/// <summary>
/// The key that is returned by the <see cref="CurrentPageKey"/> property
/// when the current Page is the root page.
/// </summary>
public const string RootPageKey = "-- ROOT --";
/// <summary>
/// The key that is returned by the <see cref="CurrentPageKey"/> property
/// when the current Page is not found.
/// This can be the case when the navigation wasn't managed by this NavigationService,
/// for example when it is directly triggered in the code behind, and the
/// NavigationService was not configured for this page type.
/// </summary>
public const string UnknownPageKey = "-- UNKNOWN --";
private readonly Dictionary<string, Type> _pagesByKey = new Dictionary<string, Type>();
/// <summary>
/// The key corresponding to the currently displayed page.
/// </summary>
public string CurrentPageKey
{
get
{
lock (_pagesByKey)
{
//var frame = ((Frame)Window.Current.Content);
var frame = MainPage.MPage.MyFrame;
if (frame.BackStackDepth == 0)
{
return RootPageKey;
}
if (frame.Content == null)
{
return UnknownPageKey;
}
var currentType = frame.Content.GetType();
if (_pagesByKey.All(p => p.Value != currentType))
{
return UnknownPageKey;
}
var item = _pagesByKey.FirstOrDefault(
i => i.Value == currentType);
return item.Key;
}
}
}
/// <summary>
/// If possible, discards the current page and displays the previous page
/// on the navigation stack.
/// </summary>
public void GoBack()
{
//var frame = ((Frame)Window.Current.Content);
var frame = MainPage.MPage.MyFrame;
if (frame.CanGoBack)
{
frame.GoBack();
}
}
/// <summary>
/// Displays a new page corresponding to the given key.
/// Make sure to call the <see cref="Configure"/>
/// method first.
/// </summary>
/// <param name="pageKey">The key corresponding to the page
/// that should be displayed.</param>
/// <exception cref="ArgumentException">When this method is called for
/// a key that has not been configured earlier.</exception>
public void NavigateTo(string pageKey)
{
NavigateTo(pageKey, null);
}
/// <summary>
/// Displays a new page corresponding to the given key,
/// and passes a parameter to the new page.
/// Make sure to call the <see cref="Configure"/>
/// method first.
/// </summary>
/// <param name="pageKey">The key corresponding to the page
/// that should be displayed.</param>
/// <param name="parameter">The parameter that should be passed
/// to the new page.</param>
/// <exception cref="ArgumentException">When this method is called for
/// a key that has not been configured earlier.</exception>
public virtual void NavigateTo(string pageKey, object parameter)
{
lock (_pagesByKey)
{
if (!_pagesByKey.ContainsKey(pageKey))
{
throw new ArgumentException(
string.Format(
"No such page: {0}. Did you forget to call NavigationService.Configure?",
pageKey),
"pageKey");
}
//var frame = ((Frame)Window.Current.Content); // 這句設定導航時用到的Frame控件為根Frame
var frame = MainPage.MPage.MyFrame;
frame.Navigate(_pagesByKey[pageKey], parameter);
}
}
/// <summary>
/// Adds a key/page pair to the navigation service.
/// </summary>
/// <param name="key">The key that will be used later
/// in the <see cref="NavigateTo(string)"/> or <see cref="NavigateTo(string, object)"/> methods.</param>
/// <param name="pageType">The type of the page corresponding to the key.</param>
public void Configure(string key, Type pageType)
{
lock (_pagesByKey)
{
if (_pagesByKey.ContainsKey(key))
{
throw new ArgumentException("This key is already used: " + key);
}
if (_pagesByKey.Any(p => p.Value == pageType))
{
throw new ArgumentException(
"This type is already configured with key " + _pagesByKey.First(p => p.Value == pageType).Key);
}
_pagesByKey.Add(
key,
pageType);
}
}
}
}