[WPF][MVVM] コードビハインドは汚さずにボタンでページ遷移する3つの方法


Hyperlink 要素を使うと NavigateUri プロパティにパスを指定することでページ遷移を実現することができますが、Button コントロールには NavigateUri プロパティがありません。
どのようにページを遷移させればよいでしょうか。

すぐに思いつくのは、ページのコードビハインドに Click イベントハンドラを実装して NavigationService.Navigate を呼び出すことです。
ただ、MVVM(Model-View-ViewModel)パターンを採用する場合、なるべくコードビハインドは汚したくありません。
ここではコードビハインドを使わずにページを遷移させる方法を3つご紹介します。

1. ビューモデルで遷移先のページインスタンスを指定する

NavigationWindow にホストされたページをコマンドバインディングで遷移させる例です。

ビューモデル

ビューからコマンドを受けて遷移を実行します。
Application.Current.MainWindow から NavigationWindow を取得し、Navigate メソッドの引数に Page インスタンスを渡しています。

ICommand の実装(RelyCommandDelegateCommand/独自 Command 実装)については説明を省略させていただきます。

public ICommand NavigateNextCommand { get; protected set; }
// :
public FirstPageViewModel()
{
    this.NavigateNextCommand = new RelayCommand<object>(this.NavigateNext);
}
// :
protected void NavigateNext(object parameter)
{
    var navigationWindow = (NavigationWindow)Application.Current.MainWindow;
    navigationWindow.Navigate(new SecondPage(), parameter);
}

ビュー

ページのXAMLでボタンにビューモデルのコマンドをバインドします。

<Page.Resources>
    <vm:FirstPageViewModel x:Key="PageViewModel" />
</Page.Resources>
<Page.DataContext>
    <StaticResourceExtension ResourceKey="PageViewModel" />
</Page.DataContext>
    :
    <Button Content="次へ" Command="{Binding NavigateNextCommand}" CommandParameter="パラメータも渡せます" />

CommandParameter でパラメータを渡すこともできます。
渡したパラメータは、たとえば NavigationService.LoadCompletedNavigationEventArgs から受け取ることができます。
(遷移先のビューモデルでパラメータを受け取るためには、それをサポートするMVVMフレームワークを採用するか、自前で渡す仕組みを実装する必要があります)

2. ビューモデルで遷移先のページ相対パスを指定する

ビューモデルでコマンドを受けた後、Navigate メソッドの引数にアプリケーションルートからの相対パスを渡します。

protected void NavigateNext(object parameter)
{
    var navigationWindow = (NavigationWindow)Application.Current.MainWindow;
    var uri = new Uri("Views/SecondPage.xaml", UriKind.Relative);
    navigationWindow.Navigate(uri, parameter);
}

ビューモデルのほかの部分やXAMLは「1」と同じです。

3. ビヘイビアを使用してXAMLで遷移先を指定する

ビヘイビアを定義しておけば、ビューモデルへの記述も不要となり、ビューで指定するだけで遷移できるようになります。

ビヘイビア

ボタンコントロールにナビゲーション機能を提供するビヘイビアを定義します。

  • 遷移先ページ指定用に Uri 型の依存関係プロパティを定義し、XAMLから指定できるようにします。
  • Click イベントハンドラで NavigationService を取得して Navigate メソッドを呼び出します。
public class NavigateButtonBehaivior : Behavior<ButtonBase>
{
    public static readonly DependencyProperty NavigatePageProperty =
        DependencyProperty.Register("NavigatePage", typeof(Uri), typeof(NavigateButtonBehaivior), new UIPropertyMetadata(null));

    public static readonly DependencyProperty NavigateExtraDataProperty =
        DependencyProperty.Register("NavigateExtraData", typeof(object), typeof(NavigateButtonBehaivior), new UIPropertyMetadata(null));

    // 遷移先のページ
    public Uri NavigatePage
    {
        get { return (Uri)GetValue(NavigatePageProperty); }

        set {  SetValue(NavigatePageProperty, value); }
    }

    // 遷移先に渡すパラメータ
    public object NavigateExtraData
    {
        get { return GetValue(NavigateExtraDataProperty); }

        set { SetValue(NavigateExtraDataProperty, value); }
    }

    protected override void OnAttached()
    {
        base.OnAttached();

        this.AssociatedObject.Click += this.AssociatedObjectClick;
    }

    protected override void OnDetaching()
    {
        this.AssociatedObject.Click -= this.AssociatedObjectClick;

        base.OnDetaching();
    }

    // クリックされたときの処理
    private void AssociatedObjectClick(object sender, RoutedEventArgs e)
    {
        if (this.NavigatePage == null)
        {
            return;
        }

        var button = (ButtonBase)sender;
        var navigationService = GetNavigationService(button);
        if (navigationService == null)
        {
            return;
        }

        // 現ページのパッケージURLを取得して相対パスを絶対パスに変換する。
        // ※new Uri(((IUriContext)navigationWindow).BaseUri, this.NavigatePage) だと
        //  ナビゲーションウィンドウXAMLからの相対パスになるので、サブディレクトリとの間で遷移できない。
        var baseUri = BaseUriHelper.GetBaseUri(button);
        var uri = new Uri(baseUri, this.NavigatePage);

        // ナビゲート
        navigationService.Navigate(uri, this.NavigateExtraData);
    }

    protected virtual NavigationService GetNavigationService(DependencyObject element)
    {
        var window = Window.GetWindow(element);
        if (window is NavigationWindow navigationWindow)
        {
            // NavigationWindow の場合
            return navigationWindow.NavigationService;
        }

        var parent = element;
        while ((parent = VisualTreeHelper.GetParent(parent)) != null)
        {
            if (parent is Frame frame)
            {
                // プレーンな(非 Navigation)Window で Frame を使用している場合
                return frame.NavigationService;
            }
        }

        return null;
    }
}

ビュー

ページのXAMLでは、Button に子要素としてナビゲーション用のビヘイビアを追加します。

<Page
    
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    xmlns:b="clr-namespace:WpfNavigation.Behaviors">

    <Page.Resources>
        <vm:FirstPageViewModel x:Key="PageViewModel" />
    </Page.Resources>
    <Page.DataContext>
        <StaticResourceExtension ResourceKey="PageViewModel" />
    </Page.DataContext>
        :
    <Button Content="次へ" >
        <i:Interaction.Behaviors>
            <b:NavigateButtonBehaivior NavigatePage="SecondPage.xaml" NavigateExtraData="パラメータも渡せます" />
        </i:Interaction.Behaviors>
    </Button>

Uri 型のプロパティを定義すると、ReSharper を導入した環境ではXAMLデザイナでリストからページを選択できるようになります。

設定されるパス文字列は現在のビューからの相対パスとなります。
パス文字列は既定のコンバーター UriTypeConverter によって Uri 型に変換されますが、そのまま Navigate メソッドに渡しても検索することができません。
Navigate メソッドには「パッケージの URI」が必要です。
ビヘイビアでは AssociatedObjectClickBaseUriHelper.GetBaseUri メソッドを使ってこのページパスの変換を行っています。