&tag(WPF,MVVM,WPFのためのMVVMデザイン パターン); *目次 [#ka793b28] #contents *参考情報 [#i940a73b] -[[WPF のための MODEL-VIEW-VIEWMODEL (MVVM) デザイン パターン:http://msdn.microsoft.com/ja-jp/magazine/dd419663.aspx]] *概要 [#sf66f6d2] -顧客情報を一覧表示するビューと顧客情報を新規追加するビューが存在。どちらもタブで表示。 *RelayCommandの役割 [#uc595673] -ViewにViewModelがバインドされ、ButtonなどにViewModelのICommandインターフェイスがバインドされる(「保存」などの機能を実行する)。 -ICommandからViewModelの内部構造に触りたい。しかしICommandをViewModelのインナークラスにすると複雑になるので、処理をリレーするためにRelayCommandを導入。 -例えばCutomerViewModelでは次のように定義されている。 #pre{{ public ICommand SaveCommand { get { if (_saveCommand == null) { _saveCommand = new RelayCommand( param => this.Save(), param => this.CanSave ); } return _saveCommand; } } }} -CustomerView.xamlでは次のようにバインドされている。 #pre{{ <!-- SAVE BUTTON --> <Button Grid.Row="8" Grid.Column="2" Command="{Binding Path=SaveCommand}" Content="_Save" HorizontalAlignment="Right" Margin="4,2" MinWidth="60" /> }} *ViewModelの構造 [#ve640666] -ViewModelは継承構造となっている。 **ViewModelBase [#i61fb092] -ViewModelの階層のルート。 -INotifyPropertyChangedインターフェイスを実装。ViewModelのプロパティが変更されるたびにPropertyChangedイベントを起動する(結果バインドされたUIが更新される)。 -サブクラスのために、OnPropertyChanged()メソッドを公開している。サブクラスから文字列ベースで変更されたプロパティ名が指定されるため、実際にそのプロパティが存在するかどうか、VerifyPropertyName()メソッドを使って確認している。 #pre{{ // In ViewModelBase.cs public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { this.VerifyPropertyName(propertyName); PropertyChangedEventHandler handler = this.PropertyChanged; if (handler != null) { var e = new PropertyChangedEventArgs(propertyName); handler(this, e); } } [Conditional("DEBUG")] [DebuggerStepThrough] public void VerifyPropertyName(string propertyName) { // Verify that the property name matches a real, // public, instance property on this object. if (TypeDescriptor.GetProperties(this)[propertyName] == null) { string msg = "Invalid property name: " + propertyName; if (this.ThrowOnInvalidPropertyName) throw new Exception(msg); else Debug.Fail(msg); } } }} -CustomewViewModelサブクラスからはつぎのように呼び出されている。 #pre{{ public string Email { get { return _customer.Email; } set { if (value == _customer.Email) return; _customer.Email = value; base.OnPropertyChanged("Email"); } } }} **CommandViewModel [#r790134d] -メインウィンドウに表示される。"View all customers", "Create new customer"などのリンクに対するViewモデル。 -コマンドをセットして実行するだけの役割。 -MainWindowViewModelで次のように生成されている。 #pre{{ public ReadOnlyCollection<CommandViewModel> Commands { get { if (_commands == null) { List<CommandViewModel> cmds = this.CreateCommands(); _commands = new ReadOnlyCollection<CommandViewModel>(cmds); } return _commands; } } List<CommandViewModel> CreateCommands() { return new List<CommandViewModel> { new CommandViewModel( Strings.MainWindowViewModel_Command_ViewAllCustomers, new RelayCommand(param => this.ShowAllCustomers())), new CommandViewModel( Strings.MainWindowViewModel_Command_CreateNewCustomer, new RelayCommand(param => this.CreateNewCustomer())) }; } }} -MainWindowResource.xamlでDateTemplateとして定義され、MainWindow.xamlに埋め込まれている。 #pre{{ <DataTemplate x:Key="CommandsTemplate"> <ItemsControl IsTabStop="False" ItemsSource="{Binding}" Margin="6,2"> <ItemsControl.ItemTemplate> <DataTemplate> <TextBlock Margin="2,6"> <Hyperlink Command="{Binding Path=Command}"> <TextBlock Text="{Binding Path=DisplayName}" /> </Hyperlink> </TextBlock> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </DataTemplate> }} **WorkspaceViewModel [#i4e571b0] -ViewModelBaseを直接継承し、「閉じる」機能を実装。 **MainWindowViewModel [#pd103d5d] -メニューから呼び出される「閉じる」機能は、AppクラスのOnStartupで設定される。 #pre{{ EventHandler handler = null; handler = delegate { viewModel.RequestClose -= handler; window.Close(); }; viewModel.RequestClose += handler; }} -顧客一覧ビュー、顧客登録ビューの各タブにある×ボタンがおされたときビューを非表示にする処理を設定するのも、MainWindowViewModelの役割。 --ObservableCollection<WorkspaceViewModel> _workspacesでタブごとのViewModelを管理。 --タブ生成時にViewModelに、CustomerViewModel、AllCustomersViewModelのインスタンスを追加。 --OnWorkspacesChanged()により、生成されたViewModelのRequestCloseにOnWorkspaceRequestClose()を設定。 --これにより、×が押されたときに、MainWindowViewModelのOnWorkspaceRequestCloseが呼び出され結果としてビューが非表示になる。 ちなみに、タブ生成時は次のコードでタブの順番を制御している。TabControlを使っているのでバインドされているWorkspacesが変更されるだけで自動的にタブは増える。 #pre{{ void SetActiveWorkspace(WorkspaceViewModel workspace) { Debug.Assert(this.Workspaces.Contains(workspace)); ICollectionView collectionView = CollectionViewSource.GetDefaultView(this.Workspaces); if (collectionView != null) collectionView.MoveCurrentTo(workspace); } }} ※[[Bea Stollnitz » WPF’s CollectionViewSource:http://bea.stollnitz.com/blog/?p=387]] *データモデルとリポジトリ [#pff35a78] -全てのCustomerはCustomerRepositoryで管理される。 -CustomerViewModelでSave()が実行されるとCustomerRepositoryに新しいCustomerが追加される。 *新しい顧客のデータ入力フォーム [#i138ca69] -CustomerView.xamlが定義。 -Customerクラス・CustomerViewModelクラスの検証メソッドにより検証される。 -Customer Typeリストボックスの初期値は"(Not Specified)"。CustomerのIsCompanyプロパティはboolがたなので困る(true or falseしかとれない)。→ViewModelを使う。 -CustomerViewModelでは次のようにリストボックスの選択肢と、CustomerTypeのアクセサを定義。 #pre{{ public string[] CustomerTypeOptions { get { if (_customerTypeOptions == null) { _customerTypeOptions = new string[] { "(Not Specified)", "Person", "Company" }; } return _customerTypeOptions; } } public string CustomerType { get { return _customerType; } set { if (value == _customerType || String.IsNullOrEmpty(value)) return; _customerType = value; if (_customerType == "Company") { _customer.IsCompany = true; } else if (_customerType == "Person") { _customer.IsCompany = false; } base.OnPropertyChanged("CustomerType"); base.OnPropertyChanged("LastName"); } } }} -保存時はCustomerクラスのIsValid()により検証している。 *顧客一覧 [#q08767ad] -AllCustomersView.xamlで定義。 -CustomerViewModelの一覧が、AllCustomersとして公開されている #pre{{ public ObservableCollection<CustomerViewModel> AllCustomers { get; private set; } }} -CustomerRepositoryを監視し、Customerが追加されたタイミングで自分の管理するAllCustomersに新しいCustomerViewModelを追加する。 #pre{{ void OnCustomerAddedToRepository(object sender, CustomerAddedEventArgs e) { var viewModel = new CustomerViewModel(e.NewCustomer, _customerRepository); this.AllCustomers.Add(viewModel); } }} -顧客が選択されたり選択解除されたりしたときのために、CustomerViewModelのPropertyChangedに、OnCustomerViewModelPropertyChangedがセットされている。 -CustomerViewModelのIsSelectedが変更された結果OnCustomerViewModelPropertyChangedが呼ばれ、そのプロパティ名が"IsSelected"なら、TotalSelectedSalesが変更されたことをUIに通達する。 #pre{{ void OnCustomerAddedToRepository(object sender, CustomerAddedEventArgs e) { var viewModel = new CustomerViewModel(e.NewCustomer, _customerRepository); this.AllCustomers.Add(viewModel); } }} *ポイント [#o666aeff] **TabControlではなくHeaderedContentControlを使っている [#q52f90d8] MainWindow.xamlで次のように定義されている。 #pre{{ <Border Grid.Column="2" Style="{StaticResource MainBorderStyle}" > <HeaderedContentControl Content="{Binding Path=Workspaces}" ContentTemplate="{StaticResource WorkspacesTemplate}" Header="Workspaces" Style="{StaticResource MainHCCStyle}" /> </Border> }}