2010年2月23日火曜日

第16回 別Windowを表示する

個人的につぶやきの表示領域は大きくとりたいので、
テキスト入力領域は別窓で開くようにしたいと思います。

ここで、Webで情報収集をしていてよく見るのですが、
MVVMに忠実に従って作っていくと、(ViewModelがViewを意識しない)
新しいWindowを表示するのに苦戦している人が多いみたいですね。

というわけで、いきなり洗練された構造を作ろうとすると時間がかかってしかたない気がしますので、
今回は「とりあえず動けばいいや」くらいのスタンスでいきたいと思います。
Windowの表示機能が2,3パターンでてくると、のちのちよりよい構造が見えてくるかもしれません。

今回のポイントは以下
1.新しいWindowの生成は、ViewModelとViewにコネクタとなるクラスを作る
2.いろんなViewModelをWindowとして表示するための
汎用WindowのGeneralWindowクラスをつくる
そのクラスに渡すために、ViewModelの基本クラスClosableViewModelBaseクラスを作る

CloseableViewModelBaseは単純にClose要求を処理できるようにしたViewModelBaseです
本家MVVM文書のWorkspaceViewModelくらいの位置づけです。

 abstract public class ClosableViewModelBase : ViewModelBase {
  /// <summary>
  /// コンストラクタ
  /// </summary>
  /// <param name="displayName">ビューでの表示名</param>
  public ClosableViewModelBase(string displayName)
   : base(displayName) {
  }

  /// <summary>
  /// Close要求
  /// </summary>
  public event EventHandler RequestClose;

  /// <summary>
  /// このViewModelを閉じる
  /// </summary>
  public void Close() {
   if (RequestClose != null) {
    RequestClose(this, EventArgs.Empty);
   }
  }
 }


他のViewModelからClose()が呼ばれると、RequestCloseを通じてViewに閉じるように要求します

GeneralViewはこのViewModelを持ちます


 public partial class GeneralView : Window {
  private ClosableViewModelBase _vm;

  public GeneralView(ClosableViewModelBase target) {
   _vm = target;
   target.RequestClose += this.OnCloseRequested;
   DataContext = target;
   InitializeComponent();
  }

  private void OnCloseRequested(object sender, EventArgs e) {
   _vm.RequestClose -= this.OnCloseRequested;
   this.Close();
  }
 }

ViewModelの通知を受け取るように登録して、
受け取ったらView自身をClose()します。
ただしXAMLの方に、入る可能性のあるViewModelのDataTemplateを
以下のようにあかかじめ登録しておく必要があります。
(ここがなんだかなーと思うところです)


 <Window.Resources>
  <DataTemplate DataType="{x:Type vm:SubmitPanelViewModel}">
   <vw:SubmitView/>
  </DataTemplate>
 </Window.Resources>



ViewModel側のコネクタは以下のようにします(EventArgsのコードは略)


 class ViewConnector {
  static private ViewConnector _viewConnector = new ViewConnector();
  static public ViewConnector Instance {
   get { return _viewConnector; }
  }

  /// <summary>
  /// 表示する
  /// </summary>
  /// <param name="target">表示したいViewModel</param>
  public void Show(ClosableViewModelBase target) {
   Debug.Assert(RequestShowWindow != null, "RequestShowWindowが初期化されていません");

   RequestShowWindow(this, new RequestShowViewModelEventArgs(target));
  }

  /// <summary>
  /// 新しいWindowでの表示要求
  /// </summary>
  public event EventHandler<RequestShowViewModelEventArgs> RequestShowWindow;
 }


Showメソッドを提供するだけです。
View側でこのイベントをキャッチします。
クラス名はなんかふさわしくないかもしれませんが、とりあえずこれで。


 class ViewFactory {
  private static ViewFactory _instance = new ViewFactory();
  public static ViewFactory Instance {
   get { return _instance; }
  }

  public void Initialize() {
   ViewConnector.Instance.RequestShowWindow += OnShowWindowRequested;
  }

  private void OnShowWindowRequested(object sender, RequestShowViewModelEventArgs e) {
   //汎用View(ウィンドウ)でViewModelを表示する
   var window = new GeneralView(e.ViewModel);
   window.WindowStartupLocation = WindowStartupLocation.CenterScreen;
   window.Show();
  }

この仕組みを使うために、コマンドから以下のように使います。


  private void ShowSubmitPanel(object parameter) {
   var vm = new SubmitPanelViewModel(_userInfo);
   Utility.ViewConnector.Instance.Show(vm);
  }


誰かのつぶやきへの返信機能もいれてみます。
これは簡単で、SubmitPanelViewModelのEditingTextに初期値を入れてShowするだけです。
(SubmitPanelViewModelのコンストラクタの違いはまず無視してください)


  private void OnReplyCommandRequested(object parameter) {
   var vm = new SubmitPanelViewModel(_userInfo,
    new TimelineViewModel("", new TimelineItemViewModel[]{this}));
   vm.EditingText = string.Format("@{0} ", Name);
   Utility.ViewConnector.Instance.Show(vm);
  }


もちろんボタン、コマンド等も追加していますが、それはソースコード見てください。
これまでのやり方と一緒です。

返信するときには、返信対象の相手の発言が見えた方が便利ですよね?
ということで、新しく表示するWindowにもタイムライン(の一部)を表示できるようにしてみます。

TimelineViewModelのコンストラクタを増やして、
中身が更新されないタイムラインも作れるようにします。


  /// <summary>
  /// コンストラクタ(このコンストラクタは時間経過で更新されない静的なタイムラインを生成する)
  /// </summary>
  /// <param name="displayName">ビューに表示するときの名前</param>
  /// <param name="initialItems">表示するアイテム</param>
  public TimelineViewModel(string displayName, IEnumerable<TimelineItemViewModel> initialItems)
   : base(displayName) {
   _allItems = new ObservableCollection<TimelineItemViewModel>(initialItems);
   _displayNameOriginal = displayName;
   _timeline = null;
  }


_timeline のnullチェックとかも追加しましたが略。
これが先程「無視してください」と言ったところの変更理由です。

XAMLのデザインは、すでに作ったやつを流用するだけで楽です。



<UserControl x:Class="WTwitter.View.SubmitView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 xmlns:vm="clr-namespace:WTwitter.ViewModel"
 xmlns:vw="clr-namespace:WTwitter.View"
 xmlns:compModel="clr-namespace:System.ComponentModel;assembly=WindowsBase"
 Loaded="UserControl_Loaded">
 <UserControl.Resources>
  <DataTemplate DataType="{x:Type vm:TimelineViewModel}">
   <vw:TimelineView/>
  </DataTemplate>
 </UserControl.Resources>
 <DockPanel>
  <DockPanel DockPanel.Dock="Bottom">
   <Button Content="投稿" Command="{Binding Path=SubmitCommand}"
     HorizontalAlignment="Right" Width="Auto" DockPanel.Dock="Right"/>
   
   <TextBox x:Name="TextArea" Text="{Binding Path=EditingText, UpdateSourceTrigger=PropertyChanged}"
      MinLines="3" TextWrapping="Wrap"/>

  </DockPanel>
  <ContentPresenter Content="{Binding Path=Timeline}"/>
 </DockPanel>
</UserControl>


Resourceの追加と、DockPanel、ContentPresenterの追加だけです。

配色がなんか変になりますけど、今回はここまでです。
ちなみに好みでアイコンファイルを変更しました。
お手数ですが、プロジェクトのreadmeファイルを参照して、取得し直してください。m(__)m

今回もブログで書き忘れていることがあるかもしれませんが、
ソースコードを見てもわからない場合はご質問ください。

今回のソースコード
http://wtwitter.codeplex.com/SourceControl/changeset/view/41642

0 件のコメント:

コメントを投稿