none
ComboBoxのバインドのタイミングについて RRS feed

  • 質問

  • お世話になります。

    また、フォーラムへの連続の質問がマナー違反であった場合、先にお詫び申し上げます。

    現在WPFでテストアプリを作成しているのですが、どのように対応すればいいのかがわかりません。

    内容としてはComboBoxで選択した項目をTextBoxに反映し、かつTextBoxにフォーカスを設定&文字選択状態にする。といった機能なのですが、下記のコードを実行すると、SelectorOnSelectionChangedイベントでMyTextBoxがNullであるためエラーが発生します。

    MainViewModelのコンストラクタでSelectedCategoryに値を設定しなければ問題ないのですが、初期選択する必要があるので値は設定する必要があります。また、SelectorOnSelectionChangedでMyTextBoxがNullだった場合に処理を抜ければ問題ないのですが、何か違う気がします。

    こうしたほうがスマート、またはこんな考え方もあるなどご意見をいただけないでしょうか。

    よろしくお願いします。

    MainWindow.xaml

    <Window x:Class="ComboboxBindingTest.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:vm="clr-namespace:ComboboxBindingTest"
            Title="MainWindow" Width="525" Height="350">
        <Window.DataContext>
            <vm:MainViewModel />
        </Window.DataContext>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition />
            </Grid.RowDefinitions>
            <ComboBox x:Name="MyComboBox"
                      Grid.Row="0"
                      ItemsSource="{Binding Categories}"
                      SelectedItem="{Binding SelectedCategory}"
                      SelectionChanged="SelectorOnSelectionChanged" />
            <TextBox x:Name="MyTextBox" Grid.Row="1"
                     Text="{Binding ElementName=MyComboBox, Path=SelectedValue}" />
        </Grid>
    </Window>
    
    MainWindow.xaml.cs
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            this.InitializeComponent();
        }
    
        private void SelectorOnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            this.MyTextBox.Focus();
            this.MyTextBox.SelectAll();
        }
    }
    
    MainViewModel.cs
    public class MainViewModel
    {
        public List<string> Categories { get; set; }
    
        public string SelectedCategory { get; set; }
    
        public MainViewModel()
        {
            this.Categories = new List<string>
            {
                "野菜",
                "果物",
                "菓子"
            };
    
            this.SelectedCategory = "野菜";
        }
    }
    

    2016年1月29日 11:09

回答

  • 初期化が完了する前にSelectionChangedイベントが発生しているので、初期化が終わってからイベントの処理をするといいです

    private void SelectorOnSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
    	if (this.IsLoaded)
    	{
    		this.MyTextBox.Focus();
    		this.MyTextBox.SelectAll();
    	}
    }

    原因としてはXAMLでDataContextを設定しているため、ウィンドウに配置されるコントロールの生成が完了してWindow内の名前の付いたコントロールをWindowが認識する前にDataContextが適用されてしまっています。
    (コンストラクタのInitializeComponent()が完了する前にDataContextが適用されている)

    回避するには上記のようにIsLoadedを見て完了していると判定するか、XAMLで設定せずに

    public partial class MainWindow : Window
    {
    	public MainWindow()
    	{
    		InitializeComponent();
    		this.DataContext = new MainViewModel();
    	}

    のようにInitializeComponent()以降の生成が終わった状態でDataContextに適用する必要があります。

    #先の質問にも関係しますが、私はViewをnewした後にコードでViewModelをDataContext設定しているので、このような初期化順でのエラーが発生しなくなります。


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

    • 編集済み gekkaMVP 2016年1月29日 11:36
    • 回答としてマーク o.yoshiak 2016年2月1日 1:15
    2016年1月29日 11:35
  • こんにちは。

    別の案ですが、ViewModelの初期化処理はコンストラクタ以外に初期化処理を用意し、Viewのレンダリング後に行うようにすることがあります。
    MVVMインフラのLivetなどはこんな作りになっていたと思います。

    <Window x:Class="WpfApplication2.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:WpfApplication2"
            xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
            xmlns:core="http://schemas.microsoft.com/expression/2010/interactions"
            Title="MainWindow" Height="350" Width="525">
        <Window.DataContext>
            <local:MainViewModel />
        </Window.DataContext>
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="ContentRendered">
                <core:CallMethodAction TargetObject="{Binding}" MethodName="Initialize" />
            </i:EventTrigger>
        </i:Interaction.Triggers>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition />
            </Grid.RowDefinitions>
            <ComboBox x:Name="MyComboBox"
                      Grid.Row="0"
                      ItemsSource="{Binding Categories}"
                      SelectedItem="{Binding SelectedCategory}"
                      SelectionChanged="MyComboBox_SelectionChanged" />
            <TextBox x:Name="MyTextBox" Grid.Row="1"
                     Text="{Binding ElementName=MyComboBox, Path=SelectedValue}" />
        </Grid>
    </Window>
    public partial class MainWindow : Window
    {
    	public MainWindow()
    	{
    		InitializeComponent();
    	}
    
    	private void MyComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
    	{
    		this.MyTextBox.Focus();
    		this.MyTextBox.SelectAll();
    	}
    }
    
    public class MainViewModel : BindableBase
    {
    	private List<String> _Categories;
    	public List<string> Categories 
    	{ 
    		get
    		{
    			return _Categories;
    		}
    		set
    		{
    			_Categories = value;
    			OnPropertyChanged("Categories");
    		}
    	}
    
    	private string _SelectedCategory;
    	public string SelectedCategory 
    	{
    		get
    		{
    			return _SelectedCategory;
    		}
    		set
    		{
    			_SelectedCategory = value;
    			OnPropertyChanged("SelectedCategory");
    		}
    	}
    
    	public MainViewModel()
    	{
    	}
    	
    	public void Initialize()
    	{
    		this.Categories = new List<string>
    		{
    			"野菜", "果物", "菓子"
    		};
    		this.SelectedCategory = "野菜";
    	}
    }
    あと、ViewModelに変更通知が入ってなかったので入れました。(OnPropertyChanged)
    インフラはPrismをNuGetして使ってます。


    2016年1月29日 11:46
    モデレータ

すべての返信

  • 初期化が完了する前にSelectionChangedイベントが発生しているので、初期化が終わってからイベントの処理をするといいです

    private void SelectorOnSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
    	if (this.IsLoaded)
    	{
    		this.MyTextBox.Focus();
    		this.MyTextBox.SelectAll();
    	}
    }

    原因としてはXAMLでDataContextを設定しているため、ウィンドウに配置されるコントロールの生成が完了してWindow内の名前の付いたコントロールをWindowが認識する前にDataContextが適用されてしまっています。
    (コンストラクタのInitializeComponent()が完了する前にDataContextが適用されている)

    回避するには上記のようにIsLoadedを見て完了していると判定するか、XAMLで設定せずに

    public partial class MainWindow : Window
    {
    	public MainWindow()
    	{
    		InitializeComponent();
    		this.DataContext = new MainViewModel();
    	}

    のようにInitializeComponent()以降の生成が終わった状態でDataContextに適用する必要があります。

    #先の質問にも関係しますが、私はViewをnewした後にコードでViewModelをDataContext設定しているので、このような初期化順でのエラーが発生しなくなります。


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

    • 編集済み gekkaMVP 2016年1月29日 11:36
    • 回答としてマーク o.yoshiak 2016年2月1日 1:15
    2016年1月29日 11:35
  • こんにちは。

    別の案ですが、ViewModelの初期化処理はコンストラクタ以外に初期化処理を用意し、Viewのレンダリング後に行うようにすることがあります。
    MVVMインフラのLivetなどはこんな作りになっていたと思います。

    <Window x:Class="WpfApplication2.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:WpfApplication2"
            xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
            xmlns:core="http://schemas.microsoft.com/expression/2010/interactions"
            Title="MainWindow" Height="350" Width="525">
        <Window.DataContext>
            <local:MainViewModel />
        </Window.DataContext>
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="ContentRendered">
                <core:CallMethodAction TargetObject="{Binding}" MethodName="Initialize" />
            </i:EventTrigger>
        </i:Interaction.Triggers>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition />
            </Grid.RowDefinitions>
            <ComboBox x:Name="MyComboBox"
                      Grid.Row="0"
                      ItemsSource="{Binding Categories}"
                      SelectedItem="{Binding SelectedCategory}"
                      SelectionChanged="MyComboBox_SelectionChanged" />
            <TextBox x:Name="MyTextBox" Grid.Row="1"
                     Text="{Binding ElementName=MyComboBox, Path=SelectedValue}" />
        </Grid>
    </Window>
    public partial class MainWindow : Window
    {
    	public MainWindow()
    	{
    		InitializeComponent();
    	}
    
    	private void MyComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
    	{
    		this.MyTextBox.Focus();
    		this.MyTextBox.SelectAll();
    	}
    }
    
    public class MainViewModel : BindableBase
    {
    	private List<String> _Categories;
    	public List<string> Categories 
    	{ 
    		get
    		{
    			return _Categories;
    		}
    		set
    		{
    			_Categories = value;
    			OnPropertyChanged("Categories");
    		}
    	}
    
    	private string _SelectedCategory;
    	public string SelectedCategory 
    	{
    		get
    		{
    			return _SelectedCategory;
    		}
    		set
    		{
    			_SelectedCategory = value;
    			OnPropertyChanged("SelectedCategory");
    		}
    	}
    
    	public MainViewModel()
    	{
    	}
    	
    	public void Initialize()
    	{
    		this.Categories = new List<string>
    		{
    			"野菜", "果物", "菓子"
    		};
    		this.SelectedCategory = "野菜";
    	}
    }
    あと、ViewModelに変更通知が入ってなかったので入れました。(OnPropertyChanged)
    インフラはPrismをNuGetして使ってます。


    2016年1月29日 11:46
    モデレータ
  • gekkaさん

    ありがとうございます。

    原因の理由まで説明していただき助かります。

    IsLoadedプロパティの存在を初めて知りました。

    DataContextに設定するタイミングを考えてみたいと思います。

    2016年2月1日 1:15
  • Tak1waさん

    ありがとうございます。

    PrismとLivetはMVVM検索すると必ず引っかかりますよね。

    その辺りも基本を理解してから勉強したいと思います。

    Triggerの使い方もなんとなくわかってきました。

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

    2016年2月1日 1:22