トップ回答者
ComboBoxのバインドのタイミングについて

質問
-
お世話になります。
また、フォーラムへの連続の質問がマナー違反であった場合、先にお詫び申し上げます。
現在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.cspublic partial class MainWindow : Window { public MainWindow() { this.InitializeComponent(); } private void SelectorOnSelectionChanged(object sender, SelectionChangedEventArgs e) { this.MyTextBox.Focus(); this.MyTextBox.SelectAll(); } }
MainViewModel.cspublic class MainViewModel { public List<string> Categories { get; set; } public string SelectedCategory { get; set; } public MainViewModel() { this.Categories = new List<string> { "野菜", "果物", "菓子" }; this.SelectedCategory = "野菜"; } }
回答
-
初期化が完了する前に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!)
-
こんにちは。
別の案ですが、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して使ってます。
- 編集済み いわさ Tak1waMVP, Moderator 2016年1月29日 11:48
- 回答としてマーク o.yoshiak 2016年2月1日 1:15
すべての返信
-
初期化が完了する前に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!)
-
こんにちは。
別の案ですが、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して使ってます。
- 編集済み いわさ Tak1waMVP, Moderator 2016年1月29日 11:48
- 回答としてマーク o.yoshiak 2016年2月1日 1:15