Keenag Blog

プログラミング備忘録ブログです。C#、WPFの記事が中心となります。

【WPF】ポップアップで入力された値を親画面で受け取る方法

今回は、WPFで親画面でポップアップを出した際に、入力された値を親画面で受け取る方法に ついてサンプルアプリケーションをつくったので、説明してきたいと思います。

各クラスの役割

各クラスの役割はこんな感じです。 なお、いくつかのクラスは省略しています。詳細はサンプルコードを参考にしてください。

クラス名 役割
MainWindow.xaml ポップアップ表示元の親画面のXAML
ChildDialog.xaml 親画面から表示されるポップアップのXAML
InputValueNotification.cs 子から親へ値の橋渡しをするNotificationクラス
MainWindowViewModel.cs ポップアップ表示元の親画面のViewModelクラス
ChildDialogViewModel.cs 親画面から表示されるポップアップのViewModelクラス

MainWindow.xaml

親画面では、ポップアップで入力された文字を表示するTextBoxが用意されています。
また、ポップアップを表示するための「ポップアップ表示」ボタンを配置されています。このボタンを押下することで、prismを使用し、PopupWindowActionでポップアップが表示されます。

<Window x:Class="NotificationSample.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:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:views="clr-namespace:NotificationSample.Views"
        prism:ViewModelLocator.AutoWireViewModel="True"
        Title="NotificationSample" Height="350" Width="525"
        Background="LightSlateGray">
    <i:Interaction.Triggers>
        <prism:InteractionRequestTrigger SourceObject="{Binding OpenChildDialogRequest}">
            <prism:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True" >
                <prism:PopupWindowAction.WindowContent>
                    <views:ChildDialog />
                </prism:PopupWindowAction.WindowContent>
            </prism:PopupWindowAction>
        </prism:InteractionRequestTrigger>
    </i:Interaction.Triggers>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="100"/>
            <RowDefinition Height="100"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="180"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Label Grid.Row="0"
               Grid.Column="0"
               Content="ポップアップで入力された文字:"
               FontSize="12"
               HorizontalAlignment="Right"
               VerticalAlignment="Bottom"/>
        <TextBox Grid.Row="0"
                 Grid.Column="1"
                 Height="30"
                 HorizontalAlignment="Left"
                 VerticalAlignment="Bottom"
                 Width="200"
                 Text="{Binding OwnerInputValue.Value}"/>
        <Button Grid.Row="1"
                Grid.Column="1"
                Content="ポップアップ表示"
                Width="150"
                Height="50"
                HorizontalAlignment="Left"
                VerticalAlignment="Center"
                Command="{Binding OpenChildDialogCommand}"/>
    </Grid>
</Window>

ChildDialog.xaml

ポップアップ画面では、親画面に引き継ぎたい文字を入力するTextBoxと、ポップアップを閉じ、親画面へ戻る「親画面へ」ボタンが配置されています。

<UserControl x:Class="NotificationSample.Views.ChildDialog"
             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"
             Width="525"
             Height="350"
             Background="LightSlateGray">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="100"/>
            <RowDefinition Height="100"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="180"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Label Grid.Row="0"
               Grid.Column="0"
               Content="親画面に引き継ぐ文字:"
               FontSize="12"
               HorizontalAlignment="Right"
               VerticalAlignment="Bottom"/>
        <TextBox Grid.Row="0"
                 Grid.Column="1"
                 Height="30"
                 HorizontalAlignment="Left"
                 VerticalAlignment="Bottom"
                 Width="200"
                 Text="{Binding ChildInputValue.Value}"/>
        <Button Grid.Row="1"
                Grid.Column="1"
                Content="親画面へ"
                Width="150"
                Height="50"
                HorizontalAlignment="Left"
                VerticalAlignment="Center"
                Command="{Binding NavigateOwnerCommand}"/>
    </Grid>
</UserControl>

InputValueNotification.cs

prismのNotificationクラスを継承したInputValueNotificationクラスには、プロパティにInputValueを定義します。このプロパティにポップアップ内のTextBoxで入力された値を詰め、親画面のViewModelで値を受け取ります。

using Prism.Interactivity.InteractionRequest;

namespace NotificationSample.ViewModels
{
    public class InputValueNotification : Notification
    {
        public string InputValue { get; set; }
    }
}

MainWindowViewModel.cs

OwnerInputValueは、ポップアップで入力された値を表示するために使用するプロパティです。
OpenChildDialogCommandは、親画面で「ポップアップ表示」ボタンを押下した際に呼ばれるCommandです。
OpenChildDialogRequestは、ポップアップの表示をViewに通知するときに使用されます。
OpenChildDialogCommandが実行されると、inputValueNotificationを引数にOpenChildDialogRequestが呼ばれ、 ポップアップが表示されます。
引数で渡したinputValueNotificationが保持するInputValueプロパティがポップアップ側で変更されていたら、OwnerInputValueに、InputValueプロパティの値をセットします。

using Prism.Commands;
using Prism.Interactivity.InteractionRequest;
using Prism.Mvvm;
using Reactive.Bindings;

namespace NotificationSample.ViewModels
{
    public class MainWindowViewModel : BindableBase
    {   
        public ReactiveProperty<string> OwnerInputValue { get; } = new ReactiveProperty<string>();

        public DelegateCommand OpenChildDialogCommand { get; }

        public InteractionRequest<InputValueNotification> OpenChildDialogRequest { get; } = new InteractionRequest<InputValueNotification>();
        
        public MainWindowViewModel()
        {
            OpenChildDialogCommand = new DelegateCommand(() =>
            {
                var inputValueNotification = new InputValueNotification() { Title = "NotificationSample" };
                OpenChildDialogRequest.Raise(inputValueNotification);
                if (inputValueNotification.InputValue != null)
                {
                    OwnerInputValue.Value = inputValueNotification.InputValue;
                }
            });

        }
    }
}

ChildDialogViewModel.cs

ChildInputValueはポップアップのTextBoxのTextにBindingされているプロパティです。 ポップアップで「親画面へ」ボタンが押下された際はNavigateOwnerCommandが実行されます。
NavigateOwnerCommandが実行された際は、ChildInputValueの値をInputValueNotificationのInputValueプロパティに詰め、FinishInteractionでポップアップを閉じます。

using Prism.Commands;
using Prism.Interactivity.InteractionRequest;
using Prism.Mvvm;
using Reactive.Bindings;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NotificationSample.ViewModels
{
    public class ChildDialogViewModel : BindableBase, IInteractionRequestAware
    {
        public INotification Notification { get; set; }
        public Action FinishInteraction { get; set; }
        public ReactiveProperty<string> ChildInputValue { get; } = new ReactiveProperty<string>();


        public DelegateCommand NavigateOwnerCommand => new DelegateCommand(() =>
        {
            ((InputValueNotification)Notification).InputValue = ChildInputValue.Value;
            FinishInteraction();
        });
    }
}

実行結果

実行結果は次のようになります。
ポップアップのTextBoxで入力された値を親画面で受け取れていることが確認できます。

サンプルコード

以下に公開しています。
https://github.com/Keenag/SampleCode/tree/master/NotificationSample

【WPF】タブ順指定ではまりかけた話

XAMLではTabIndexを指定することで、ユーザのタブ操作によるフォーカスの順番を設定することができますが、 Windowの中にUserControlを表示するような画面の場合、少しはまりかけたので、備忘録として残します。 以下サンプルコードで説明していきます。
なお、今回も一部のソースコードの説明は割愛しています。詳細はサンプルコードを参考にしてください。

MainWindow.xaml

MainWindow.xamlクラスは、WindowのLoadedイベントで、MainwindowViewModel.csのNavigateCommandが呼ばれ、ContentControl部分にMainWindowViewModel.csで指定したUserControlが組み込まれるという作りになっています。

<Window x:Class="TabIndexSample.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:prism="http://prismlibrary.com/"
        prism:ViewModelLocator.AutoWireViewModel="True"
        Title="TabIndexSample" Height="350" Width="525">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Loaded">
            <i:InvokeCommandAction Command="{Binding NavigateCommand}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <Grid>
        <ContentControl prism:RegionManager.RegionName="ContentRegion"
                        FocusVisualStyle="{StaticResource FocusVisualStyleKey}"/>
    </Grid>
</Window>

HogeUserControl.xaml

HogeUserControl.xamlは、MainWindow.xamlのContentControlに表示されるUserControlで、 TabIndexでユーザのタブ操作を制御しています。

<UserControl x:Class="TabIndexSample.Views.HogeUserControl"
             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"
             Height="350"
             Width="525"
             Background="LightSlateGray">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="100"/>
            <RowDefinition Height="100"/>
            <RowDefinition Height="100"/>
        </Grid.RowDefinitions>
        <Label Grid.Row="0"
               Content="TabIndex1"
               HorizontalAlignment="Left"
               VerticalAlignment="Center"
               Margin="90,0,0,0"/>
        <TextBox Grid.Row="0"
                 Width="200"
                 Height="30"
                 TabIndex="1"
                 FocusVisualStyle="{StaticResource FocusVisualStyleKey}"/>
        <Label Grid.Row="1"
               Content="TabIndex2"
               HorizontalAlignment="Left"
               VerticalAlignment="Center"
               Margin="90,0,0,0"/>
        <TextBox Grid.Row="1"
                 Width="200"
                 Height="30"
                 TabIndex="2"
                 FocusVisualStyle="{StaticResource FocusVisualStyleKey}"/>
        <Label Grid.Row="2"
               Content="TabIndex0"
               HorizontalAlignment="Left"
               VerticalAlignment="Center"
               Margin="90,0,0,0"/>
        <TextBox Grid.Row="2"
                 Width="200"
                 Height="30"
                 TabIndex="0"
                 FocusVisualStyle="{StaticResource FocusVisualStyleKey}"/>
    </Grid>
</UserControl>

実行結果(修正前)

このソースコードで実行すると、実行結果はこのようになります。

HogeUserControl.xaml内のTab操作を終えると、タブのフォーカスが画面全体にあたってしまいます。
これでは、意図していたタブ操作ではありません。 そこで、HogeUserControlのUserControlタグに

IsTabStop="False"

を追加してみたのですが、結果は変わりません。
UserContorolの表示元であるMainWindow.xamlのContentControlにIsTabStop="False"を追加することで解決することができました。

        <ContentControl prism:RegionManager.RegionName="ContentRegion"
                        FocusVisualStyle="{StaticResource FocusVisualStyleKey}"
                        IsTabStop="False"/>

実行結果(修正後)

修正後の実行結果はこのようになります。
無事、画面全体へのフォーカスが外れました。

サンプルコード

以下に公開しています。
https://github.com/Keenag/SampleCode/tree/master/TabIndexSample

【WPF】ReactivePropertyを使用してボタンの活性非活性を制御する方法

ReactivePropertyはMVVM+リアクティブプログラミングを快適にサポートしてくれるライブラリです。
今回は、ReactivePropertyを使用して、ボタンの制御サンプルを作ってみたので、備忘録として残したいと思います。

なお、ReactivePropertyについてもっと知りたい方は、MVVMをリアクティブプログラミングで快適にReactivePropertyオーバービューをご覧になってみてください。

では、サンプルプログラムを見ていきましょう。

MainWindow.xaml

MainWindow.xamlクラスには、ボタンの活性、非活性を制御するグルーピングされた「活性」ラジオボタン、「非活性」ラジオボタンを配置します。 「活性」ラジオボタンのIsCheckedプロパティには、IsActiveをバインディングしています。 ボタンには、CommandプロパティにHogeCommandをバインディングしています。

<Window x:Class="ReactiveCommandSample.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/"
        prism:ViewModelLocator.AutoWireViewModel="True"
        Title="ReactiveCommandSample"
        Height="350"
        Width="525"
        Background="LightSlateGray">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="80"/>
            <RowDefinition Height="40"/>
            <RowDefinition Height="50"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="200"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <RadioButton Grid.Row="0"
                     Grid.Column="1"
                     Content="活性"
                     VerticalAlignment="Bottom"
                     HorizontalAlignment="Left"
                     TabIndex="0"
                     HorizontalContentAlignment="Left"
                     VerticalContentAlignment="Center"
                     Padding="1"
                     GroupName="HogeRadio"
                     IsChecked="{Binding IsActive.Value}"
                     Height="27"
                     Width="126"
                     FocusVisualStyle="{x:Null}"
                     />
        <RadioButton Grid.Row="1"
                     Grid.Column="1"
                     Content="非活性"
                     VerticalAlignment="Bottom"
                     HorizontalAlignment="Left"
                     TabIndex="1"
                     VerticalContentAlignment="Center"
                     GroupName="HogeRadio"
                     Height="27"
                     Width="139"
                     FocusVisualStyle="{x:Null}"
                     />
        <Button Grid.Row="2"
                Grid.Column="1"
                Content="Button"
                HorizontalAlignment="Left"
                VerticalAlignment="Bottom"
                Width="99"
                Height="34"
                Command="{Binding HogeCommand}"/>
    </Grid>
</Window>

MainWindowViewModel.cs

MainWindowViewModelクラスでは、「活性」ボタンの変更通知を受け取るReactiveProperttyのIsActiveを定義します。また、ReactiveCommandのHogeCommandを定義します。
HogeCommandは、IsActiveがTrueの時のみ(「活性」ボタンが選択されている時のみ)活性化し、押下された際はOnClickメソッドが呼ばれます。

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

namespace ReactiveCommandSample.ViewModels
{
    public class MainWindowViewModel : BindableBase
    {
        public ReactiveProperty<bool> IsActive { get; } = new ReactiveProperty<bool>(true);
        public ReactiveCommand HogeCommand { get; }

        public MainWindowViewModel()
        {
            HogeCommand = IsActive.Select(x => x == true).ToReactiveCommand();
            HogeCommand.Subscribe(OnClick);
        }

        public void OnClick()
        {
            //ボタンが押下された際の処理
        }
    }
}

実行結果

実行結果はこのようになります。
「活性」ラジオボタンが押下されているときは、ボタンが活性化しています。 f:id:Keenag:20171008225521p:plain

「非活性」ラジオボタンが押下されているときは、ボタンが非活性化しています。 f:id:Keenag:20171008225611p:plain

サンプルコード

以下に公開しています。
https://github.com/Keenag/SampleCode/tree/master/ReactiveCommandSample

【WPF】1画面に複数のUserControlを可変で表示する方法

WPFで1つの画面の中に複数のUseControlを可変で表示させたいという要件があり、
以下のように実装することで実現することができました。

各クラスの役割

各クラスの役割はこんな感じです。 なお、いくつかのクラスは省略しています。詳細はサンプルコードを参考にしてください。

クラス名 役割
DisplayControlViewModel.cs 画面に表示させるUserControlのViewModelを抽象化したクラス
ComboBoxControlViewModel.cs DisplayControlViewmodelを継承したComboBoxControl.xaml のViewModel
ComboBoxControl.xaml ComboBoxを持ったUserControlクラス
MainWindowViewModel.cs MainWindow.xamlのViewModel
MainWindow.xaml 複数のUserControlを表示するWindow

DisplayControlViewModel.cs

画面に表示させるUserControlに紐づくViewModelクラスを抽象化したクラスです。

namespace DisplayUserControlListSample.ViewModels
{
    public abstract class DisplayControlViewModel
    {
    }
}

ComboBoxControlViewModel.cs

ComboBoxControl.xamlのViewModelクラスです。DisplayControlViewModelを継承しています。
コンボボックスに表示されるItemのコレクションを持ち、コンストラクタで値を初期化しています。

using Reactive.Bindings;

namespace DisplayUserControlListSample.ViewModels
{
    public class ComboBoxControlViewModel : DisplayControlViewModel
    {
        /// <summary>
        /// コンボボックスに表示されるItemのコレクション
        /// </summary>
        public ReactiveCollection<string> Items { get; }

        public ComboBoxControlViewModel()
        {
            Items = new ReactiveCollection<string>() { "Apple", "Banana", "Peach" };
        }
    }
}

ComboBoxControl.xaml

1つのComboBoxのみをもつXAMLです。ItemsSourceにComboBoxControlViewModelからItemsをバインドしています。

<UserControl x:Class="DisplayUserControlListSample.Views.ComboBoxControl"
             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"
             Width="300"
             Height="30">
    <Grid Margin="0,0,0,5" >
        <StackPanel Orientation="Horizontal">
            <ComboBox Height="30"
                      Width="300"
                      FontFamily="MS Gothic"
                      FontSize="12"
                      HorizontalAlignment="Center"
                      VerticalAlignment="Center"
                      ItemsSource="{Binding Items}"
                       >
            </ComboBox>
        </StackPanel>
    </Grid>
</UserControl>

MainWindowViewModel.cs

DisplayControlViewModelクラスのコレクションをプロパティとしてもちます。このプロパティに表示したいUserControlに紐づいたViewModelのインスタンスを追加します。
※なお、本エントリーではTextBoxControlに関する説明は割愛しています

using Prism.Mvvm;
using Reactive.Bindings;

namespace DisplayUserControlListSample.ViewModels
{
    public class MainWindowViewModel : BindableBase
    {
        public ReactiveCollection<DisplayControlViewModel> DisplayControlViewModels { get; } = new ReactiveCollection<DisplayControlViewModel>();

        public MainWindowViewModel()
        {
            DisplayControlViewModels.Clear();
            DisplayControlViewModels.Add(new ComboBoxControlViewModel());
            DisplayControlViewModels.Add(new TextBoxControlViewModel());
            DisplayControlViewModels.Add(new ComboBoxControlViewModel());
        }
    }
}

MainWindow.xaml

ItemsControl要素のItemsSourceプロパティにDisplayControlViewModelのリストをバインドします。
WindowのリソースにDataTemplateで表示するUserControlを定義します。
ItemsSourceにバインドされたDisplayControlViewModelのTypeを見て表示するUserControlを切り替えます。

<Window x:Class="DisplayUserControlListSample.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:views="clr-namespace:DisplayUserControlListSample.Views"
        xmlns:viewModels="clr-namespace:DisplayUserControlListSample.ViewModels"
        xmlns:prism="http://prismlibrary.com/"
        prism:ViewModelLocator.AutoWireViewModel="True"
        Height="350" Width="525"
        Title="DisplayUserControlListSample"
        Background="LightSlateGray">
    <Window.Resources>
        <DataTemplate DataType="{x:Type viewModels:TextBoxControlViewModel}">
            <views:TextBoxControl/>
        </DataTemplate>
        <DataTemplate DataType="{x:Type viewModels:ComboBoxControlViewModel}">
            <views:ComboBoxControl/>
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <ScrollViewer  Margin="50,50,50,50"
                       HorizontalScrollBarVisibility="Auto"
                       VerticalScrollBarVisibility="Auto"
                       IsTabStop="False">
            <ItemsControl ItemsSource="{Binding DisplayControlViewModels}"
                          VerticalAlignment="Top"
                          IsTabStop="False" />
        </ScrollViewer>
    </Grid>
</Window>

実行結果

実行結果はこのようになります。
MainWindowViewModel.csで追加したViewmodelに紐づくUserControlが表示されます。
f:id:Keenag:20171008174037p:plain

サンプルコード

以下に公開しています。
https://github.com/Keenag/SampleCode/tree/master/DisplayUserControlListSample