none
親画面から子画面の表示/非表示を切り替えたい RRS feed

  • 質問

  • いつもお世話になっております。 arubi-momoと申します。

    開発環境は以下の通りとなっております。

    Windows7 64bit VisualStudio2017 Professional WPF C# .netFramework4.6.1

    アプリケーション起動時に、App.xaml.csの中で、メインウィンドウ(親)と子画面A、B、Cを同時に生成しており、子画面A、B、Cは非表示で起動しています。

    メインウィンドウのチェックボックスA、B、CそれぞれのON/OFFのタイミングで、子画面の表示/非表示を切り替えられるようにしたいと考えております。

    MVVMパターンを使用しているため、チェックボックスのコマンド処理で実現したいと考えていますが、うまくいきません。

    また、実装方法を色々調べているうちに、ViewModelから別のViewの操作をするのはよくない?などの情報があったり、ビヘイビアを作成してそちらで処理をすべき?などと迷いはじめ、迷走してきてしまいました。

    WPFを始めたばかりでかなり初歩的な質問をしているかと思いますが、良い実装方法をご存知の方がいらっしゃいましたら、助けていただければと思います。

    勉強不足で大変申し訳ありませんが、何卒、よろしくお願いいたします。

    2018年11月22日 10:53

回答

  • MainのViewModelの中に子ウィンドウのViewModelを持てばいいのでは?
    MainのWindowの外に子ウィンドウで表示と、MainWindowの中で子パネルに表示とでは、見え方が違うだけなので。


    <Window x:Class="WpfApp1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:WpfApp1"
            mc:Ignorable="d"
            Title="MainWindow" SizeToContent="WidthAndHeight" TextElement.FontSize="30">
        <Grid>
            <ItemsControl ItemsSource="{Binding Path=SubWindows}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <CheckBox IsChecked="{Binding Path=IsVisible,Mode=TwoWay}" Content="{Binding Path=Label}" />
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </Grid>
    </Window>
    <Window x:Class="WpfApp1.WindowA" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:WpfApp1" mc:Ignorable="d"
            Title="WindowA" Width="100" Height="100" ShowInTaskbar="False"
            Closing="Window_Closing">
        <Window.Resources>
            <BooleanToVisibilityConverter x:Key="b2v"/>
        </Window.Resources>
        
        <Window.Visibility>
            <Binding Path="IsVisible" Converter="{StaticResource b2v}" Mode="TwoWay"/>
        </Window.Visibility>
        
        <Grid>
            <TextBlock Text="WindowA" />
        </Grid>
    </Window>
    namespace WpfApp1
    {
        public partial class WindowA : System.Windows.Window
        {
    
            public WindowA()
            {
                InitializeComponent();
            }
    
            private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
            {
                e.Cancel=true;
               this.SetValue(VisibilityProperty, System.Windows.Visibility.Collapsed);
            }
        }
    }

    using System;
    using System.ComponentModel;
    using System.Windows;
    
    namespace WpfApp1
    {
        public partial class App : Application
        {
            //App.xamlのビルドアクションをPageに
            //App.xamlのStartUpUriは削除
    
            [STAThread]
            public static void Main()
            {
                App app = new App();
                app.InitializeComponent();
    
                MainWindowViewModel mainViewModel = new MainWindowViewModel();
                SubWindowViewModel subViewModelA = new SubWindowViewModel() { Label = "A" };
                SubWindowViewModel subViewModelB = new SubWindowViewModel() { Label = "B" };
                SubWindowViewModel subViewModelC = new SubWindowViewModel() { Label = "C" };
    
                mainViewModel.SubWindows.Add(subViewModelA);
                mainViewModel.SubWindows.Add(subViewModelB);
                mainViewModel.SubWindows.Add(subViewModelC);
    
                MainWindow mainWindow = new MainWindow(){ DataContext=mainViewModel};
    
                mainWindow.Loaded+=(s,e)=>
                {
                    Window windowA = new WindowA() { Owner = mainWindow, DataContext = subViewModelA };
                    Window windowB = new WindowB() { Owner = mainWindow, DataContext = subViewModelB };
                    Window windowC = new WindowC() { Owner = mainWindow, DataContext = subViewModelC };
                };
    
                app.Run(mainWindow);
            }
        }
    
        class ModelBase : System.ComponentModel.INotifyPropertyChanged
        {
            public event PropertyChangedEventHandler PropertyChanged;
            protected void OnPropertyChanged(string name)
            {
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
            }
        }
    
        class MainWindowViewModel : ModelBase
        {
            public MainWindowViewModel()
            {
                this.SubWindows = new System.Collections.ObjectModel.ObservableCollection<SubWindowViewModel>();
            }
    
            //MainViewModelの中に従属するViewModelを持たせてみる
            public System.Collections.ObjectModel.ObservableCollection<SubWindowViewModel> SubWindows { get; set; }
        }
    
        class SubWindowViewModel : ModelBase
        {
            public bool IsVisible
            {
                get
                {
                    return _IsVisible;
                }
                set
                {
                    _IsVisible = value;
                    OnPropertyChanged(nameof(IsVisible));
                }
            }
            private bool _IsVisible;
    
            public string Label { get; set; }
        }
    }

    チェックボックスにプロパティをバインディングで済むので、コマンドをバインドにはしてません。

    コマンドでやるなら以下のようにしてViewModelに依存しないようにできます。

    <Window x:Class="WpfApp1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:WpfApp1"
            xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
            mc:Ignorable="d"
            Title="MainWindow" SizeToContent="WidthAndHeight" TextElement.FontSize="30">
        <Window.Resources>
            <local:WindowA x:Key="winA" />
            <local:WindowA x:Key="winB"/>
            <local:WindowA x:Key="winC"/>
        </Window.Resources>
        <Grid>
            <StackPanel>
                <CheckBox Content="WindowA" >
                    <!--System.Windows.Interactivity 参照-->
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="Checked">
                            <i:InvokeCommandAction Command="{x:Static ApplicationCommands.Open}" CommandParameter="{StaticResource winA}"/>
                        </i:EventTrigger>
    
                        <i:EventTrigger EventName="Unchecked">
                            <i:InvokeCommandAction Command="{x:Static ApplicationCommands.Close}" CommandParameter="{StaticResource winA}"/>
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </CheckBox>
            </StackPanel>
        </Grid>
    </Window>
    using System;
    using System.ComponentModel;
    using System.Windows;
    using System.Windows.Input;
    namespace WpfApp1
    {
        public partial class App : Application
        {
            //ビルドアクションはApplicationDefinitionのままで
    
            protected override void OnStartup(StartupEventArgs e)
            {
                var cbOpen = new CommandBinding(ApplicationCommands.Open, OpenExecuted, CanOpenExecute);
                System.Windows.Input.CommandManager.RegisterClassCommandBinding(typeof(Window), cbOpen);
    
    
                var cbClose = new CommandBinding(ApplicationCommands.Close, CloseExecuted, CanCloseExecute);
                System.Windows.Input.CommandManager.RegisterClassCommandBinding(typeof(Window), cbClose);
    
    
                base.OnStartup(e);
            }
    
            private static void CanOpenExecute(object sender, CanExecuteRoutedEventArgs e)
            {
                var w = e.Parameter as Window;
                if (w != null)
                {
                    e.CanExecute = w.Owner == null || w.Visibility != Visibility.Visible;
                }
            }
            private static void OpenExecuted(object sender, ExecutedRoutedEventArgs e)
            {
                var w = e.Parameter as Window;
                if (w != null)
                {
                    if (w.Owner == null)
                    {
                        w.Owner = App.Current.MainWindow;
                    }
                    w.Visibility = Visibility.Visible;
                }
            }
    
    
            private static void CanCloseExecute(object sender, CanExecuteRoutedEventArgs e)
            {
                var w = e.Parameter as Window;
                if (w != null)
                {
                    e.CanExecute =  w.Visibility == Visibility.Visible;
                }
            }
            private static void CloseExecuted(object sender, ExecutedRoutedEventArgs e)
            {
                var w = e.Parameter as Window;
                if (w != null)
                {
                    w.Visibility = Visibility.Collapsed;
                }
            }
        }
    }


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 編集済み gekkaMVP 2018年11月23日 5:16 コマンド利用の場合のコード追加
    • 回答としてマーク arubi_momo 2018年11月26日 14:50
    2018年11月23日 4:08
  • こちらで出てくる、(s,e)というのは何でしょうか。どこにも宣言されておらず、理解ができませんでした。

    ラムダ式について勉強してみるといいでしょう。
    簡単に言うと、ここで関数を宣言していて、その関数の省略形として引数部分だけになってます。
    通常の関数での引数は関数定義部分で名前が宣言されますが、同様にここが関数の定義箇所であり引数の名前の宣言箇所となります。

    using System;
    using System.ComponentModel;
    using System.Windows;
    using System.Windows.Input;
    namespace WpfApp1
    {
        public partial class App : Application
        {
            //ビルドアクションはApplicationDefinitionのままで
    
            [STAThread]
            public static void Main()
            {
                App app = new App();
                app.InitializeComponent();
    
                MainWindowViewModel mainViewModel = new MainWindowViewModel();
                SubWindowViewModel subViewModelA = new SubWindowViewModel() { Label = "A" };
                SubWindowViewModel subViewModelB = new SubWindowViewModel() { Label = "B" };
                SubWindowViewModel subViewModelC = new SubWindowViewModel() { Label = "C" };
    
                mainViewModel.SubWindows.Add(subViewModelA);
                mainViewModel.SubWindows.Add(subViewModelB);
                mainViewModel.SubWindows.Add(subViewModelC);
    
                MainWindow mainWindow = new MainWindow() { DataContext = mainViewModel };
    
                //この書き方はもっとも基本的なイベント登録のやりかたで、別に定義してあるMainWindow_Loadedという関数をイベントに登録してます
                //RoutedEventHandlerというのはLoadedイベントの型です。
                //イベントの登録は型が一致していないとできません。
                mainWindow.Loaded += new RoutedEventHandler(MainWindow_Loaded);
    
                //Loadedイベントの定義からRoutedEventHandlerと明示しなくても自動で判断して変換してくれるので省略できます
                mainWindow.Loaded += MainWindow_Loaded;
    
    
                //この書き方は内部に定義してある匿名の関数をイベントに登録しています
                mainWindow.Loaded += new RoutedEventHandler((object s, RoutedEventArgs e) =>
                {
                    //関数内部に定義すると、関数内で定義してあるmainWindowやsubViewModelAといった変数が参照できます。
                    Window windowA = new WindowA() { Owner = mainWindow, DataContext = subViewModelA };
                    Window windowB = new WindowB() { Owner = mainWindow, DataContext = subViewModelB };
                    Window windowC = new WindowC() { Owner = mainWindow, DataContext = subViewModelC };
                });
    
                //Loadedイベントの定義からRoutedEventHandlerと明示しなくても自動で判断して変換してくれるので省略できます
                mainWindow.Loaded += (object s, RoutedEventArgs e) =>
                {
                    Window windowA = new WindowA() { Owner = mainWindow, DataContext = subViewModelA };
                    Window windowB = new WindowB() { Owner = mainWindow, DataContext = subViewModelB };
                    Window windowC = new WindowC() { Owner = mainWindow, DataContext = subViewModelC };
                };
    
                //この書き方は同様にLoadedイベントの定義から引数の型も自明であるから型名を省略できます
                mainWindow.Loaded += (sender, args) => //通常の関数と同様に引数名には自分で好きな名前を付けることができます
                {
                    Window windowA = new WindowA() { Owner = mainWindow, DataContext = subViewModelA };
                    Window windowB = new WindowB() { Owner = mainWindow, DataContext = subViewModelB };
                    Window windowC = new WindowC() { Owner = mainWindow, DataContext = subViewModelC };
                };
    
                app.Run(mainWindow);
            }
    
            private static void MainWindow_Loaded(object sender, RoutedEventArgs e)
            {
                //外の関数に定義すると、関数内で停止してあるmainWindowやsubViewModelAといった変数は参照できません。
                //参照しようとするとクラスのフィールドとして一時的に設定するなど面倒なことになります。
                Window windowA = new WindowA() { Owner = mainWindow, DataContext = subViewModelA };
                Window windowB = new WindowB() { Owner = mainWindow, DataContext = subViewModelB };
                Window windowC = new WindowC() { Owner = mainWindow, DataContext = subViewModelC };
            }


    参考にしたサイトではICommandというインターフェースを利用した書き方が多く、そちらの方で調べていました。CommandBindingというのはApplicationCommandsで用意されているコマンドと、アプリ内で実装したOpenExecuted、CanOpenExecutedをバインドした、という解釈で合っていますでしょうか。

    ApplicationCommandsに以外に、ComponentCommandsNavigationCommandsMediaCommandsといったクラスにも定義されているRoutedCommandをバインドしたということのがより正しいです。
    RoutedCommandはICommandから派生しているクラスですからICommandの特殊化された使い方になります。

    CommandBindingは対応するRoutedCommandに対してイベントとしてCanExecuteとExecuteを処理します。

    Xamlで定義すると以下のようになります。
    WPFのイベントがRoutedEventと名付けられているのと同じで、RoutedCommandはイベントの伝播のように内側から外側に向かってCommandBindingが見つかるまで遡ります。
    (なお、先のサンプルコードでRegisterClassCommandBindingを使っているのは全てのWindowにCommandBindingを登録していると考えてください。)

    <Window x:Class="WpfApp1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:WpfApp1"
            xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
            mc:Ignorable="d"
            Title="MainWindow" Width="300" SizeToContent="Height" TextElement.FontSize="30">
    
        <Window.CommandBindings>
            <CommandBinding Command="{x:Static ApplicationCommands.Copy}" 
                                        CanExecute="Window_CanCopyExecute"
                                        Executed="Window_CopyExecuted"/>
        </Window.CommandBindings>
    
        <Grid>
            <StackPanel>
    
                <Grid>
                    <!-- ボタンAでのコマンドがWindowのCommandBindingsで捕捉されて処理されます -->
                    <Button Command="{x:Static ApplicationCommands.Copy}"  Margin="10"
                            Content="ボタンA" >
    
                    </Button>
                </Grid>
    
                <Grid>
                    <Grid.CommandBindings>
                        <!-- ボタンBのCopyはGridで処理されるのでWindowには伝わりません -->
                        <CommandBinding Command="{x:Static ApplicationCommands.Copy}" 
                                        CanExecute="Grid_CanCopyExecute"
                                        Executed="Grid_CopyExecuted"/>
                    </Grid.CommandBindings>
                    <Button Command="{x:Static ApplicationCommands.Copy}" Margin="10"
                        Content="ボタンB">
    
                    </Button>
                </Grid>
                
                <TextBox Margin="10" TextWrapping="Wrap" AcceptsReturn="true"
                         Text="TextBoxでコマンドが処理される。ただし、TextBoxはデフォルトでCopyを処理するので外側には伝わりません">
                    <TextBox.CommandBindings>
                        <CommandBinding Command="{x:Static ApplicationCommands.Copy}" 
                                        CanExecute="TextBox_CanCopyExecute"
                                        Executed="TextBox_CopyExecuted"/>
                    </TextBox.CommandBindings>
                </TextBox>
            </StackPanel>
        </Grid>
    </Window>
    namespace WpfApp1
    {
        using System;
        using System.Windows;
        using System.Windows.Controls;
        using System.Windows.Input;
    
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
            }
    
            private void Window_CanCopyExecute(object sender, CanExecuteRoutedEventArgs e)
            {
                e.CanExecute = true;
            }
    
            private void Window_CopyExecuted(object sender, ExecutedRoutedEventArgs e)
            {
                MessageBox.Show("Window\r\n" + e.OriginalSource.GetType().ToString());
            }
    
            private void Grid_CanCopyExecute(object sender, CanExecuteRoutedEventArgs e)
            {
                e.CanExecute = true;
            }
    
            private void Grid_CopyExecuted(object sender, ExecutedRoutedEventArgs e)
            {
                MessageBox.Show("Grid\r\n" + e.OriginalSource.GetType().ToString());
            }
    
            private void TextBox_CanCopyExecute(object sender, CanExecuteRoutedEventArgs e)
            {
                e.CanExecute = true;
            }
    
            private void TextBox_CopyExecuted(object sender, ExecutedRoutedEventArgs e)
            {
                TextBox txb = e.OriginalSource as TextBox;
                MessageBox.Show(txb.Text);
            }
        }
    }
    ICommandとCommandBindingの使い分けは一般的にはどのようにするものなんでしょうか。
    • RoutedCommandではないICommandは、WindowsFormsのイベントのようにそこだけで捕捉できれば済む用途
    • RoutedCommandとCommandBindingの組み合わせは、WPFのイベントのように外側でも捕捉できるようにしたい用途。
      カスタムコントロールやユーザーコントロールなどの挙動をコマンドとして外部に公開したい用途
    • RoutedUICommandならば、さらに共通のラベル(Copyコマンドにコピーと表示)やアイコンを表示させたいような用途

    と考えればいいです。



    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 編集済み gekkaMVP 2018年11月25日 11:30
    • 回答としてマーク arubi_momo 2018年11月26日 14:50
    2018年11月25日 11:28

すべての返信

  • MainのViewModelの中に子ウィンドウのViewModelを持てばいいのでは?
    MainのWindowの外に子ウィンドウで表示と、MainWindowの中で子パネルに表示とでは、見え方が違うだけなので。


    <Window x:Class="WpfApp1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:WpfApp1"
            mc:Ignorable="d"
            Title="MainWindow" SizeToContent="WidthAndHeight" TextElement.FontSize="30">
        <Grid>
            <ItemsControl ItemsSource="{Binding Path=SubWindows}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <CheckBox IsChecked="{Binding Path=IsVisible,Mode=TwoWay}" Content="{Binding Path=Label}" />
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </Grid>
    </Window>
    <Window x:Class="WpfApp1.WindowA" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:WpfApp1" mc:Ignorable="d"
            Title="WindowA" Width="100" Height="100" ShowInTaskbar="False"
            Closing="Window_Closing">
        <Window.Resources>
            <BooleanToVisibilityConverter x:Key="b2v"/>
        </Window.Resources>
        
        <Window.Visibility>
            <Binding Path="IsVisible" Converter="{StaticResource b2v}" Mode="TwoWay"/>
        </Window.Visibility>
        
        <Grid>
            <TextBlock Text="WindowA" />
        </Grid>
    </Window>
    namespace WpfApp1
    {
        public partial class WindowA : System.Windows.Window
        {
    
            public WindowA()
            {
                InitializeComponent();
            }
    
            private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
            {
                e.Cancel=true;
               this.SetValue(VisibilityProperty, System.Windows.Visibility.Collapsed);
            }
        }
    }

    using System;
    using System.ComponentModel;
    using System.Windows;
    
    namespace WpfApp1
    {
        public partial class App : Application
        {
            //App.xamlのビルドアクションをPageに
            //App.xamlのStartUpUriは削除
    
            [STAThread]
            public static void Main()
            {
                App app = new App();
                app.InitializeComponent();
    
                MainWindowViewModel mainViewModel = new MainWindowViewModel();
                SubWindowViewModel subViewModelA = new SubWindowViewModel() { Label = "A" };
                SubWindowViewModel subViewModelB = new SubWindowViewModel() { Label = "B" };
                SubWindowViewModel subViewModelC = new SubWindowViewModel() { Label = "C" };
    
                mainViewModel.SubWindows.Add(subViewModelA);
                mainViewModel.SubWindows.Add(subViewModelB);
                mainViewModel.SubWindows.Add(subViewModelC);
    
                MainWindow mainWindow = new MainWindow(){ DataContext=mainViewModel};
    
                mainWindow.Loaded+=(s,e)=>
                {
                    Window windowA = new WindowA() { Owner = mainWindow, DataContext = subViewModelA };
                    Window windowB = new WindowB() { Owner = mainWindow, DataContext = subViewModelB };
                    Window windowC = new WindowC() { Owner = mainWindow, DataContext = subViewModelC };
                };
    
                app.Run(mainWindow);
            }
        }
    
        class ModelBase : System.ComponentModel.INotifyPropertyChanged
        {
            public event PropertyChangedEventHandler PropertyChanged;
            protected void OnPropertyChanged(string name)
            {
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
            }
        }
    
        class MainWindowViewModel : ModelBase
        {
            public MainWindowViewModel()
            {
                this.SubWindows = new System.Collections.ObjectModel.ObservableCollection<SubWindowViewModel>();
            }
    
            //MainViewModelの中に従属するViewModelを持たせてみる
            public System.Collections.ObjectModel.ObservableCollection<SubWindowViewModel> SubWindows { get; set; }
        }
    
        class SubWindowViewModel : ModelBase
        {
            public bool IsVisible
            {
                get
                {
                    return _IsVisible;
                }
                set
                {
                    _IsVisible = value;
                    OnPropertyChanged(nameof(IsVisible));
                }
            }
            private bool _IsVisible;
    
            public string Label { get; set; }
        }
    }

    チェックボックスにプロパティをバインディングで済むので、コマンドをバインドにはしてません。

    コマンドでやるなら以下のようにしてViewModelに依存しないようにできます。

    <Window x:Class="WpfApp1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:WpfApp1"
            xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
            mc:Ignorable="d"
            Title="MainWindow" SizeToContent="WidthAndHeight" TextElement.FontSize="30">
        <Window.Resources>
            <local:WindowA x:Key="winA" />
            <local:WindowA x:Key="winB"/>
            <local:WindowA x:Key="winC"/>
        </Window.Resources>
        <Grid>
            <StackPanel>
                <CheckBox Content="WindowA" >
                    <!--System.Windows.Interactivity 参照-->
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="Checked">
                            <i:InvokeCommandAction Command="{x:Static ApplicationCommands.Open}" CommandParameter="{StaticResource winA}"/>
                        </i:EventTrigger>
    
                        <i:EventTrigger EventName="Unchecked">
                            <i:InvokeCommandAction Command="{x:Static ApplicationCommands.Close}" CommandParameter="{StaticResource winA}"/>
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </CheckBox>
            </StackPanel>
        </Grid>
    </Window>
    using System;
    using System.ComponentModel;
    using System.Windows;
    using System.Windows.Input;
    namespace WpfApp1
    {
        public partial class App : Application
        {
            //ビルドアクションはApplicationDefinitionのままで
    
            protected override void OnStartup(StartupEventArgs e)
            {
                var cbOpen = new CommandBinding(ApplicationCommands.Open, OpenExecuted, CanOpenExecute);
                System.Windows.Input.CommandManager.RegisterClassCommandBinding(typeof(Window), cbOpen);
    
    
                var cbClose = new CommandBinding(ApplicationCommands.Close, CloseExecuted, CanCloseExecute);
                System.Windows.Input.CommandManager.RegisterClassCommandBinding(typeof(Window), cbClose);
    
    
                base.OnStartup(e);
            }
    
            private static void CanOpenExecute(object sender, CanExecuteRoutedEventArgs e)
            {
                var w = e.Parameter as Window;
                if (w != null)
                {
                    e.CanExecute = w.Owner == null || w.Visibility != Visibility.Visible;
                }
            }
            private static void OpenExecuted(object sender, ExecutedRoutedEventArgs e)
            {
                var w = e.Parameter as Window;
                if (w != null)
                {
                    if (w.Owner == null)
                    {
                        w.Owner = App.Current.MainWindow;
                    }
                    w.Visibility = Visibility.Visible;
                }
            }
    
    
            private static void CanCloseExecute(object sender, CanExecuteRoutedEventArgs e)
            {
                var w = e.Parameter as Window;
                if (w != null)
                {
                    e.CanExecute =  w.Visibility == Visibility.Visible;
                }
            }
            private static void CloseExecuted(object sender, ExecutedRoutedEventArgs e)
            {
                var w = e.Parameter as Window;
                if (w != null)
                {
                    w.Visibility = Visibility.Collapsed;
                }
            }
        }
    }


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 編集済み gekkaMVP 2018年11月23日 5:16 コマンド利用の場合のコード追加
    • 回答としてマーク arubi_momo 2018年11月26日 14:50
    2018年11月23日 4:08
  • gekkaさま

    いつもお世話になっております。ご回答ありがとうございます。

    Xaml側にWindow.Visibilityのプロパティを持たせて、バインディングできるなんて初めて知りました。イベントなどの処理は、コマンドなどでViewModel側にやらせなければいけない、と凝り固まっていました。こちらの方法の方が、コード量も節約でき、とても効率的であるように思いました。ありがとうございます。

    ところで、コードを拝見して2点ほどわからないところがあったため、質問してもよろしいでしょうか。

    mainWindow.Loaded+=(s,e)=>
    {
            Window windowA = new WindowA() { Owner = mainWindow, DataContext = subViewModelA };
            Window windowB = new WindowB() { Owner = mainWindow, DataContext = subViewModelB };
            Window windowC = new WindowC() { Owner = mainWindow, DataContext = subViewModelC };
    };

    こちらで出てくる、(s,e)というのは何でしょうか。どこにも宣言されておらず、理解ができませんでした。

    また、コマンド処理も私がまだよく理解できていないのが原因なのですが、こちらのコマンドの書き方を初めて見ました。

    参考にしたサイトではICommandというインターフェースを利用した書き方が多く、そちらの方で調べていました。CommandBindingというのはApplicationCommandsで用意されているコマンドと、アプリ内で実装したOpenExecuted、CanOpenExecutedをバインドした、という解釈で合っていますでしょうか。

    ICommandとCommandBindingの使い分けは一般的にはどのようにするものなんでしょうか。

    勉強不足で大変恐縮ですが、ご教示いただければ幸いです。

    何卒、よろしくお願いいたします。

    2018年11月25日 8:04
  • こちらで出てくる、(s,e)というのは何でしょうか。どこにも宣言されておらず、理解ができませんでした。

    ラムダ式について勉強してみるといいでしょう。
    簡単に言うと、ここで関数を宣言していて、その関数の省略形として引数部分だけになってます。
    通常の関数での引数は関数定義部分で名前が宣言されますが、同様にここが関数の定義箇所であり引数の名前の宣言箇所となります。

    using System;
    using System.ComponentModel;
    using System.Windows;
    using System.Windows.Input;
    namespace WpfApp1
    {
        public partial class App : Application
        {
            //ビルドアクションはApplicationDefinitionのままで
    
            [STAThread]
            public static void Main()
            {
                App app = new App();
                app.InitializeComponent();
    
                MainWindowViewModel mainViewModel = new MainWindowViewModel();
                SubWindowViewModel subViewModelA = new SubWindowViewModel() { Label = "A" };
                SubWindowViewModel subViewModelB = new SubWindowViewModel() { Label = "B" };
                SubWindowViewModel subViewModelC = new SubWindowViewModel() { Label = "C" };
    
                mainViewModel.SubWindows.Add(subViewModelA);
                mainViewModel.SubWindows.Add(subViewModelB);
                mainViewModel.SubWindows.Add(subViewModelC);
    
                MainWindow mainWindow = new MainWindow() { DataContext = mainViewModel };
    
                //この書き方はもっとも基本的なイベント登録のやりかたで、別に定義してあるMainWindow_Loadedという関数をイベントに登録してます
                //RoutedEventHandlerというのはLoadedイベントの型です。
                //イベントの登録は型が一致していないとできません。
                mainWindow.Loaded += new RoutedEventHandler(MainWindow_Loaded);
    
                //Loadedイベントの定義からRoutedEventHandlerと明示しなくても自動で判断して変換してくれるので省略できます
                mainWindow.Loaded += MainWindow_Loaded;
    
    
                //この書き方は内部に定義してある匿名の関数をイベントに登録しています
                mainWindow.Loaded += new RoutedEventHandler((object s, RoutedEventArgs e) =>
                {
                    //関数内部に定義すると、関数内で定義してあるmainWindowやsubViewModelAといった変数が参照できます。
                    Window windowA = new WindowA() { Owner = mainWindow, DataContext = subViewModelA };
                    Window windowB = new WindowB() { Owner = mainWindow, DataContext = subViewModelB };
                    Window windowC = new WindowC() { Owner = mainWindow, DataContext = subViewModelC };
                });
    
                //Loadedイベントの定義からRoutedEventHandlerと明示しなくても自動で判断して変換してくれるので省略できます
                mainWindow.Loaded += (object s, RoutedEventArgs e) =>
                {
                    Window windowA = new WindowA() { Owner = mainWindow, DataContext = subViewModelA };
                    Window windowB = new WindowB() { Owner = mainWindow, DataContext = subViewModelB };
                    Window windowC = new WindowC() { Owner = mainWindow, DataContext = subViewModelC };
                };
    
                //この書き方は同様にLoadedイベントの定義から引数の型も自明であるから型名を省略できます
                mainWindow.Loaded += (sender, args) => //通常の関数と同様に引数名には自分で好きな名前を付けることができます
                {
                    Window windowA = new WindowA() { Owner = mainWindow, DataContext = subViewModelA };
                    Window windowB = new WindowB() { Owner = mainWindow, DataContext = subViewModelB };
                    Window windowC = new WindowC() { Owner = mainWindow, DataContext = subViewModelC };
                };
    
                app.Run(mainWindow);
            }
    
            private static void MainWindow_Loaded(object sender, RoutedEventArgs e)
            {
                //外の関数に定義すると、関数内で停止してあるmainWindowやsubViewModelAといった変数は参照できません。
                //参照しようとするとクラスのフィールドとして一時的に設定するなど面倒なことになります。
                Window windowA = new WindowA() { Owner = mainWindow, DataContext = subViewModelA };
                Window windowB = new WindowB() { Owner = mainWindow, DataContext = subViewModelB };
                Window windowC = new WindowC() { Owner = mainWindow, DataContext = subViewModelC };
            }


    参考にしたサイトではICommandというインターフェースを利用した書き方が多く、そちらの方で調べていました。CommandBindingというのはApplicationCommandsで用意されているコマンドと、アプリ内で実装したOpenExecuted、CanOpenExecutedをバインドした、という解釈で合っていますでしょうか。

    ApplicationCommandsに以外に、ComponentCommandsNavigationCommandsMediaCommandsといったクラスにも定義されているRoutedCommandをバインドしたということのがより正しいです。
    RoutedCommandはICommandから派生しているクラスですからICommandの特殊化された使い方になります。

    CommandBindingは対応するRoutedCommandに対してイベントとしてCanExecuteとExecuteを処理します。

    Xamlで定義すると以下のようになります。
    WPFのイベントがRoutedEventと名付けられているのと同じで、RoutedCommandはイベントの伝播のように内側から外側に向かってCommandBindingが見つかるまで遡ります。
    (なお、先のサンプルコードでRegisterClassCommandBindingを使っているのは全てのWindowにCommandBindingを登録していると考えてください。)

    <Window x:Class="WpfApp1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:WpfApp1"
            xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
            mc:Ignorable="d"
            Title="MainWindow" Width="300" SizeToContent="Height" TextElement.FontSize="30">
    
        <Window.CommandBindings>
            <CommandBinding Command="{x:Static ApplicationCommands.Copy}" 
                                        CanExecute="Window_CanCopyExecute"
                                        Executed="Window_CopyExecuted"/>
        </Window.CommandBindings>
    
        <Grid>
            <StackPanel>
    
                <Grid>
                    <!-- ボタンAでのコマンドがWindowのCommandBindingsで捕捉されて処理されます -->
                    <Button Command="{x:Static ApplicationCommands.Copy}"  Margin="10"
                            Content="ボタンA" >
    
                    </Button>
                </Grid>
    
                <Grid>
                    <Grid.CommandBindings>
                        <!-- ボタンBのCopyはGridで処理されるのでWindowには伝わりません -->
                        <CommandBinding Command="{x:Static ApplicationCommands.Copy}" 
                                        CanExecute="Grid_CanCopyExecute"
                                        Executed="Grid_CopyExecuted"/>
                    </Grid.CommandBindings>
                    <Button Command="{x:Static ApplicationCommands.Copy}" Margin="10"
                        Content="ボタンB">
    
                    </Button>
                </Grid>
                
                <TextBox Margin="10" TextWrapping="Wrap" AcceptsReturn="true"
                         Text="TextBoxでコマンドが処理される。ただし、TextBoxはデフォルトでCopyを処理するので外側には伝わりません">
                    <TextBox.CommandBindings>
                        <CommandBinding Command="{x:Static ApplicationCommands.Copy}" 
                                        CanExecute="TextBox_CanCopyExecute"
                                        Executed="TextBox_CopyExecuted"/>
                    </TextBox.CommandBindings>
                </TextBox>
            </StackPanel>
        </Grid>
    </Window>
    namespace WpfApp1
    {
        using System;
        using System.Windows;
        using System.Windows.Controls;
        using System.Windows.Input;
    
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
            }
    
            private void Window_CanCopyExecute(object sender, CanExecuteRoutedEventArgs e)
            {
                e.CanExecute = true;
            }
    
            private void Window_CopyExecuted(object sender, ExecutedRoutedEventArgs e)
            {
                MessageBox.Show("Window\r\n" + e.OriginalSource.GetType().ToString());
            }
    
            private void Grid_CanCopyExecute(object sender, CanExecuteRoutedEventArgs e)
            {
                e.CanExecute = true;
            }
    
            private void Grid_CopyExecuted(object sender, ExecutedRoutedEventArgs e)
            {
                MessageBox.Show("Grid\r\n" + e.OriginalSource.GetType().ToString());
            }
    
            private void TextBox_CanCopyExecute(object sender, CanExecuteRoutedEventArgs e)
            {
                e.CanExecute = true;
            }
    
            private void TextBox_CopyExecuted(object sender, ExecutedRoutedEventArgs e)
            {
                TextBox txb = e.OriginalSource as TextBox;
                MessageBox.Show(txb.Text);
            }
        }
    }
    ICommandとCommandBindingの使い分けは一般的にはどのようにするものなんでしょうか。
    • RoutedCommandではないICommandは、WindowsFormsのイベントのようにそこだけで捕捉できれば済む用途
    • RoutedCommandとCommandBindingの組み合わせは、WPFのイベントのように外側でも捕捉できるようにしたい用途。
      カスタムコントロールやユーザーコントロールなどの挙動をコマンドとして外部に公開したい用途
    • RoutedUICommandならば、さらに共通のラベル(Copyコマンドにコピーと表示)やアイコンを表示させたいような用途

    と考えればいいです。



    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 編集済み gekkaMVP 2018年11月25日 11:30
    • 回答としてマーク arubi_momo 2018年11月26日 14:50
    2018年11月25日 11:28
  • gekkaさま

    いつもお世話になっております。

    大変ご丁寧なご回答をいただき、本当にありがとうございます。

    カスタムコントロールやユーザーコントロールなどの挙動をコマンドとして外部に公開したい用途

    RoutedCommandはWPFで用意されているコマンドだけではなく、自分で作成することができるんですね。RoutedCommandではないICommandより汎用性があるように思いました。

    ラムダ式もWPFではよく見かけていましたが、きちんと理解しておりませんでした。勉強してみます。

    また、前回の返信では質問ばかりしてしまいましたが、CheckBoxにプロパティをバインドする方法で問題は解決いたしました!

    ありがとうございました。

    今後ともどうぞ、よろしくお願いいたします。

    2018年11月26日 14:50