&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 &#187; 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>
}}



トップ   編集 差分 履歴 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS