Help us understand the problem. What is going on with this article?

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

CodeOne
【品質と生産性にこだわるシステム開発】 .NET(C#/VB.NET)専門・リモート開発歴10年。即日・1時間から頼める常駐しないエンジニア。確かな技術で開発チームを手堅くサポートします。
https://codeone.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした