質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.32%

  • C#

    7729questions

    C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

  • WPF

    738questions

    Windows Presentation Foundation (WPF) は、魅力的な外観のユーザー エクスペリエンスを持つ Windows クライアント アプリケーションを作成するための次世代プレゼンテーション システムです

ViewModelを持つUserControlのCommandの処理内容をFormに記述する方法

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 440

SeaFood

score 10

 環境

VisualStudio2017(C#)
.NetFramework 4.7.2
Prism.Wpf 6.3.0
ReactiveProperty 5.0.0

 前提・実現したいこと

ViewModelを持つUserControl上にあるボタンを押下した際に、Bindingされたコマンドの
内容をFormのViewModelに記述したい
使い所は、複数画面を持つシステムの、各画面にファンクションキー12個分のボタンを持つ
UserControlを配置し、各ボタンの処理は各画面のViewModelに記述したい
そういった処理を行いたい場合、どのように行ったらよろしいでしょうか?

イメージ説明

 試してみたソース

UserControlのXAML(上の画像とは異なります)

<UserControl x:Class="UserControlSample.Views.PrismUserControl1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
             xmlns:prism="http://prismlibrary.com/" 
             prism:ViewModelLocator.AutoWireViewModel="True">
    <Button Width="50" Height="50" Command="{Binding CommandExecute}" Content="ボタン" />
</UserControl>

UserControlのViewModel

using Prism.Mvvm;

namespace UserControlSample.ViewModels
{
    public class PrismUserControl1ViewModel : BindableBase
    {
        public PrismUserControl1ViewModel()
        {
        }
    }
}


FormのXAML

<Window x:Class="UserControlSample.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:prism="http://prismlibrary.com/"
        xmlns:u="clr-namespace:UserControlSample.Views"
        Title="{Binding Title}"
        Width="525"
        Height="350"
        prism:ViewModelLocator.AutoWireViewModel="True">
    <StackPanel>
        <u:PrismUserControl1 />
    </StackPanel>
</Window>


FormのViewModel

using Prism.Mvvm;
using Reactive.Bindings;
using System;

namespace UserControlSample.ViewModels
{
    public class MainWindowViewModel : BindableBase
    {
        public ReactiveCommand CommandExecute { get; set; } = new ReactiveCommand();

        public MainWindowViewModel()
        {
            this.CommandExecute.Subscribe(Xxx);
        }

        private void Xxx()
        {
            // ボタン押下時にこの処理が行われるようにしたいが、このソースだとここは動かない
            Console.WriteLine("終了ボタン押下");
        }
    }
}

 試したこと

UserControlのViewModelを削除し、Viewだけにすれば、上記「終了ボタン押下」が実行されるが
UserControlにViewModelがあると、Xxxメソッドが呼び出されない

 【追記】

 コマンド部分を別クラスに分けて MainWindowViewModel / PrismUserControl1ViewModel の両方に注入する方法。

UserControlプロジェクトにコマンドのみのクラス追加

using Reactive.Bindings;

namespace PrismUserControl1
{
    public class FunctionCommand
    {
        public ReactiveCommand CommandExecute { get; set; } = new ReactiveCommand();
    }
}


UserControlのViewModelにあるコンストラクタで上記コマンドのクラスを受け取るようにする

using Prism.Mvvm;

namespace PrismUserControl1.ViewModels
{
    public class PrismUserControl1ViewModel : BindableBase
    {
        // PrismUserControl1 でバインドして使えるようにアクセサを設ける。
        public FunctionCommand FunctionCommand { get; set; }

        public PrismUserControl1ViewModel()
        {
        }

        public PrismUserControl1ViewModel(FunctionCommand functionCommand)
        {
            // アクセサに設定する。
            FunctionCommand = functionCommand;
        }
    }
}

FormのViewModelもUserControlと同様にコンストラクタでFunctionComandを受け取るようにする

using Prism.Mvvm;
using PrismUserControl1;
using System;

namespace UserControlSample.ViewModels
{
    public class MainWindowViewModel : BindableBase
    {
        // PrismUserControl1 でバインドして使えるようにアクセサを設ける。
        public FunctionCommand FunctionCommand { get; set; }

        public MainWindowViewModel()
        {
        }

        public MainWindowViewModel(FunctionCommand functionCommand)
        {
            // アクセサに設定する。
            FunctionCommand = functionCommand;

            // CommandExecuteに処理のわりあて
            FunctionCommand.CommandExecute.Subscribe(Xxx);
        }

        private void Xxx()
        {
            Console.WriteLine("ボタン押下");
        }
    }
}


ConfigureContainerでFunctionCommandを注入できるようにする

using Microsoft.Practices.Unity;
using Prism.Modularity;
using Prism.Unity;
using PrismUserControl1;
using System.Windows;
using UserControlSample.Views;

namespace UserControlSample
{
    internal class Bootstrapper : UnityBootstrapper
    {
        protected override DependencyObject CreateShell()
        {
            return Container.Resolve<MainWindow>();
        }

        protected override void InitializeShell()
        {
            Application.Current.MainWindow.Show();
        }

        protected override void ConfigureModuleCatalog()
        {
            var moduleCatalog = (ModuleCatalog)ModuleCatalog;
            //moduleCatalog.AddModule(typeof(YOUR_MODULE));
        }

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

            // FunctionCommandを設定できるようにする
            Container.RegisterType<FunctionCommand>(new ContainerControlledLifetimeManager());
        }
    }
}
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+1

Prism を使っているなら話が早いです。
掲示されているソースコードから ViewModelLocator を使っていることも読み取れます。
それを前提とした上で、以下の実装を追加することで実現できます。

  • Prism の DI コンテナに MainWindowViewModel をシングルトンとして扱うよう指示する。
  • PrismUserControl1ViewModel に MainWindowViewModel のアクセサを設ける。
  • PrismUserControl1ViewModel のコンストラクタに引数を追加し、DI コンテナからシングルトンの MainWindowViewModel インスタンスを受け取る入口を作る。
  • PrismUserControl1 に PrismUserControl1ViewModel.MainWindowViewModel のコマンドをバインドする。

具体的な実装は以下の通りです。

 Bootstrapper.cs

using Microsoft.Practices.Unity;
using Prism.Unity;

namespace UserControlSample
{
    // DI コンテナが Unity 以外の場合は適宜読み替えること。
    public class Bootstrapper : UnityBootstrapper
    {
        protected override void ConfigureContainer()
        {
            base.ConfigureContainer();

            // Prism の DI コンテナに MainWindowViewModel をシングルトンとして扱うよう指示する。
            // こうすることで MainWindow 自身が持つインスタンスと PrismUserControl1ViewModel に DI されるインスタンスが同一(シングルトン)のものにできる。
            // この設定をしないとそれぞれ個別に new されたインスタンスを参照する形となる。
            Container.RegisterType<MainWindowViewModel>(new ContainerControlledLifetimeManager());
        }

        // ...
    }
}

 PrismUserControl1ViewModel.cs

using Prism.Mvvm;

namespace UserControlSample.ViewModels
{
    public class PrismUserControl1ViewModel : BindableBase
    {
        // PrismUserControl1 でバインドして使えるようにアクセサを設ける。
        public MainWindowViewModel MainWindowViewModel { get; set; }

        // Visual Studio のデザイナのお怒りを鎮めるために、デフォルトコンストラクタは残しておいた方が良いかも。
        public PrismUserControl1ViewModel()
        {
        }

        // コンストラクタインジェクション用に MainWindowViewModel を引数に加える。
        // これだけで DI コンテナが勝手に設定してくれる。
        public UserControl1ViewModel(MainWindowViewModel mainWindowViewModel)
        {
            // アクセサに設定する。
            MainWindowViewModel = mainWindowViewModel;
        }
    }
}

 MainWindow

<UserControl x:Class="UserControlSample.Views.PrismUserControl1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
             xmlns:prism="http://prismlibrary.com/" 
             prism:ViewModelLocator.AutoWireViewModel="True">
    <!-- MainWindowViewModel アクセサ経由で CommandExecute をバインド! -->
    <Button Width="50" Height="50" Command="{Binding MainWindowViewModel.CommandExecute}" Content="ボタン" />
</UserControl>

こういうことが簡単にできる。それが Prism の DI コンテナの真価であり神髄!
ViewModelLocator の意義はこの DI コンテナパワーを自然と ViewModel に届けることにあると私は考えている。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/06/30 09:03

    ご回答ありがとうございます。
    ご提示いただいた方法で実装してみて動くことを確認しました。
    ありがとうございます。

    以下、実装していて2点疑問点がありました。
    対応方法はなにかありますでしょうか?
    また、そもそも考え方が間違っている等ありましたら、ご指摘いただけないでしょうか。

    1.最終的なアプリの形態としては、UserControlとFormを別のプロジェクト
      として管理したいと考えています。そうなった際に
      UserControlはMainWindowViewModelを知っている必要があるので
      UserControlはFormのプロジェクトを参照する必要がある。
      また、FormはForm上にUserControlを配置するのでUserControlを参照する
      必要がある。
      結果、循環参照となり参照ができませんでした。
      Q.別プロジェクトの場合でもうまく動作させる方法はありますでしょうか?

    2.UserControlは複数のFormから呼ばれることになる為
      Bootstrapper.csにあるConfigureContainerに画面数分の(ViewModel数分の)
      「Container.RegisterType<MainWindowViewModel>(new ContainerControlledLifetimeManager());」
      を記述することになる?
      そもそも、UserControlのXAMLのCommandに
      「{Binding MainWindowViewModel.CommandExecute}」を書く必要があるので、複数の画面ViewModelを指定する事ができない?
      Q.複数の画面(複数ViewModel)からでも動作させる事は可能でしょうか?

    キャンセル

  • 2018/06/30 10:21 編集

    それぞれに回答します。

    1. あります。一般的な循環参照の解決法を使えば良いです。

    具体的な方法には複数の選択肢があります。

    ・コマンド部分を別クラスに分けて MainWindowViewModel / PrismUserControl1ViewModel の両方に注入する方法。
     コマンド部分のクラスを UserControl 側に実装すれば循環参照が消える。
    ・コマンド部分をインターフェイスにし MainWindowViewModel にそのインターフェイスを実装させる方法。
     PrismUserControlViewModel はそのインターフェイスのインスタンスを受け取る形にする。
     コマンド部分のインターフェイスを UserControl プロジェクト側に実装すれば循環参照が消える。

    また、関連としてもしかしたら Prism の Region や Module といった機能を必要としているのかもしれません。

    2. Container.RegisterType1行の記述で複数の画面で動作します。

    回答の本文で例示した Container.RegisterType の1文は、MainWindowViewModel をシングルトンとして扱えという命令です。
    シングルトンという単語がソース上に出てこないのでぱっと見わかりづらいですが、
    クラス名(ContainerControlledLifetimeManager)的には「MainWindowViewModel インスタンスの寿命を DI コンテナの寿命と一緒にする」という命令であり、
    総じてその1つの MainWindowViewModel が全てのインジェクションで共有されるという動作になり、結果、シングルトンになるみたいな感じです。
    DI コンテナ上だけの話なので、自分で個別に new したらシングルトンじゃなくなります。
    そういう命令なので1行の記述で複数の画面にインジェクションできます。

    > そもそも考え方が間違っている等ありましたら、ご指摘いただけないでしょうか。

    なぜ「ViewModelを持つUserControl上にあるボタンを押下した際に、Bindingされたコマンドの内容をFormのViewModelに記述したい」と思っているのかという根本の部分が一番の疑問です。
    技術的には可能ということで解決法を掲示しましたが、正直なところそこがまず間違っているのでは?という雰囲気を感じています。
    前提を変えたら、こんなことしなくてももっと簡単な問題だったりするかもしれません。

    キャンセル

  • 2018/06/30 23:56

    早速のご回答ありがとうございます。

    まずは
    >なぜ「ViewModelを持つUserControl上にあるボタンを押下した際に、
    >Bindingされたコマンドの内容をFormのViewModelに記述したい」と思っているのか
    について、回答させていただきます。

    画像で提示させていただいている通り、複数画面にファンクションキー想定のボタンが
    あるようなアプリの開発を想定しています。
    そして、このボタンのContentの値は各画面から、UserControlが持つ依存プロパティに
    例えば画面IDを渡すことにより、UserControlのModelで、DBや定義ファイル(XML等)から
    取得することを想定しています。
    Contentの内容を外部に持たせたい理由は
    ・複数人で開発するプロジェクトで、一箇所にボタンのContentの内容を集約する事により各画面での
     表記ゆれ、例えば、マスタメンテ画面のデータ登録時に人によってContentの内容を「保存」とする人も
     いれば「登録」とする人もいる、というような事を防ぐ
    ・開発スキルの無い人間(例えば開発スキルのない設計者)でもボタンのContentの内容を
     変更する事が可能になる
    ・Contentの内容を一括で変更したいとき、例えば、旧仕様で「保存」だったものを
     新仕様で「登録」にしたい場合、各画面を修正せず、外部ファイルでの定義なら全画面分のContentの
     修正を1ファイルを一括置換で対応可能になる
    こういった事を実現させたる事ができる為、UserControlにViewModelとModelを持たせて、そのModelに
    ボタンのContent取得処理を記述し、結果をViewModelに渡すのが良いのかなぁと考えています。
    そして、実際のファンクションキー押下時の処理は各画面ごとに異なるので
    UserControl側ではなく、各画面側で記述できるようにしたい。といった事を考えています。

    キャンセル

  • 2018/06/30 23:57

    >・コマンド部分を別クラスに分けて MainWindowViewModel / PrismUserControl1ViewModel の両方に注入する方法。 
     →ご教示いただいた内容で実装してみました。想定している挙動になりました。
      質問に追記しています、こんな感じで認識あっていますでしょうか?

    >・コマンド部分をインターフェイスにし MainWindowViewModel にそのインターフェイスを実装させる方法。
     →実装方法がわかりませんでした。
      Container.RegisterTypeで型をInterfaceに指定すると
      コンパイルは通りますが、実行時にInterfaceは指定できないと怒られてしまいます。
      どういった記述をすればよろしいでしょうか?

    キャンセル

  • 2018/06/30 23:58

    >2. Container.RegisterType1行の記述で複数の画面で動作します。
     →ご説明いただいた内容理解できました。ありがとうございます。

    キャンセル

  • 2018/07/01 01:17

    コメントでやりとり可能なレベルを超えてきたように思いますw
    なのでざっくり可能な範囲で回答しますね。

    >>・コマンド部分を別クラスに分けて MainWindowViewModel / PrismUserControl1ViewModel の両方に注入する方法。
    > →ご教示いただいた内容で実装してみました。想定している挙動になりました。
    >  質問に追記しています、こんな感じで認識あっていますでしょうか?

    あっています。

    >>・コマンド部分をインターフェイスにし MainWindowViewModel にそのインターフェイスを実装させる方法。
    > →実装方法がわかりませんでした。
    >  Container.RegisterTypeで型をInterfaceに指定すると
    >  コンパイルは通りますが、実行時にInterfaceは指定できないと怒られてしまいます。
    >  どういった記述をすればよろしいでしょうか?

    ・インターフェイス IMainWindowViewModel を作り、MainWindowViewModel に実装する。
    ・PrismUserControl1ViewModel のコンストラクタ・アクセサの型を IMainWindowViewModel にする。
    ・DI コンテナに IMainWindowViewModel のインスタンスを登録する。具体的には以下のように書く。

    Container.RegisterType<MainWindowViewModel>(new ContainerControlledLifetimeManager());
    Container.RegisterInstance<IMainWIndowViewModel>(Container.Resolve<MainWindowViewModel>());

    色々聞いた感じ、このインターフェイスの方法は恐らく今回使いません。こんな方法もあると覚えておく程度でいいでしょう。

    ----

    これまでを踏まえてですが「Form(=MainWindow であってる?)のViewModel」である必要はないと思います。
    Content を統一する目的であればそれ用の ViewModel をシングルトンとして作り、必要な ViewModel に注入すればいいです。
    例えば ContentViewModel とかの名前で作ります。MainWindowViewModel より名前も明確でわかりやすいです。
    今回 FunctionCommand を作った方法と同じですから応用でしたいことが自由にできるでしょう。
    MainWindow でも使用するのであれば MainWindow にも同じものを注入すれば良いと思います。
    他にも横断的にまとめたいものがあれば専用の ViewModel にわかりやすい名前を付けて作り、注入すれば良いでしょう。

    最後に一個突っ込みをいれたいとすれば Content の文字の統一といったら、Resources に定義したものを使うのが普通な気が…というぐらいですかねぇ…。

    ・参考:Resourcesに定義した文字列を XAML で使う方法(https://tnakamura.hatenablog.com/entry/20100928/xaml_properties_resources)

    キャンセル

  • 2018/07/01 10:07

    色々とご教示ありがとうございました。
    まだインターフェイスでの実装はできてないので、これからやってみます。

    キャンセル

同じタグがついた質問を見る

  • C#

    7729questions

    C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

  • WPF

    738questions

    Windows Presentation Foundation (WPF) は、魅力的な外観のユーザー エクスペリエンスを持つ Windows クライアント アプリケーションを作成するための次世代プレゼンテーション システムです