辿り着いた先にある物は何だろう?
随分とクリアになってきたと思う、断片的な物事が繋がってきた、まるでジグソーパズルのピースが収まって行く感じだ。しかしまだ問題と課題は山ほど残っているぞ、気を引き締めろ!
WPFでTwitterのタイムラインを表示する(MVVM版)
どうなった?
ViewとViewModelとModelを別々のコンポーネントで作成している、それぞれが下のレイヤーのコンポーネントに依存する形になっている。
以前から考えていた、プレゼンテーションコードの中でイテレーション処理されるリストオブジェクトが、MVVMではXAMLへのバインドで表現される。Microsoftはこうも見事に問題を解決しくれるのだ!考えが少し楽になった、やはり優れた設計概念に触れなければ進歩が無い。C# いや.NET Frameworkでのアプリ実装は既にOOPの概念を超えている、OOPは実装のひとつの要素に過ぎないのだ。
イベントの通知コマンドに使うデリゲートを考えて見て欲しい、.NETでは、継承や委譲さえもまどろっこしいのだ。
View.xaml
<!-- /////////////////////////////////////////////////////////////////////////////// // // WPF MVVM サンプル View マークアップです。 // // Copyright 2009 hiroxpepe // Author hiroxpepe <hiroxpepe@gmail.com> // Version 1.0.0 // Since 09.08.16 // Update 09.08.16 --> <Window x:Class="MVVMProtoType.Views.View" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:wtk="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit" Title="MVVM Proto" Height="480" Width="600"> <StackPanel Orientation="Vertical" VerticalAlignment="Top" Margin="10"> <!-- ユーザ名入力とデータ取得ボタン --> <Border BorderBrush="DarkGray" BorderThickness="1" Padding="5" CornerRadius="5"> <StackPanel Orientation="Horizontal"> <TextBlock Text="ユーザー名:" VerticalAlignment="center"/> <TextBox Width="100"> <Binding Path="UserName" UpdateSourceTrigger="PropertyChanged"/> </TextBox> <Button Command="{Binding SelectCommand}">データ取得</Button> </StackPanel> </Border> <Separator Height="10" Visibility="Hidden" /> <!-- XMLデータ表示用のグリッドコントロール --> <Border BorderBrush="DarkGray" BorderThickness="1" Padding="5" CornerRadius="5"> <wtk:DataGrid ItemsSource="{Binding Path=DataGrid, UpdateSourceTrigger=PropertyChanged}" AutoGenerateColumns="False"> <wtk:DataGrid.Columns> <wtk:DataGridTextColumn Binding="{Binding Date}" Header="日付時刻"/> <wtk:DataGridTextColumn Binding="{Binding ID}" Header="投稿ID"/> <wtk:DataGridTextColumn Binding="{Binding Client}" Header="クライアント"/> <wtk:DataGridTextColumn Binding="{Binding Text}" Header="テキスト"/> </wtk:DataGrid.Columns> </wtk:DataGrid> </Border> </StackPanel> </Window>
MVVMのView部分 コードビハインド
View.xaml.cs
using System.Windows; using MVVMProtoType.ViewModels; /////////////////////////////////////////////////////////////////////////////// // // WPF MVVM サンプル View コードです。 // // Copyright 2009 hiroxpepe // Author hiroxpepe <hiroxpepe@gmail.com> // Version 1.0.0 // Since 09.08.16 // Update 09.08.16 namespace MVVMProtoType.Views { /// <summary> /// View.xaml の相互作用ロジック /// </summary> public partial class View : Window { /////////////////////////////////////////////////////////////////////// #region コンストラクタ /// <summary> /// 引数なしコンストラクタを実行します。 /// </summary> public View() { InitializeComponent(); // ViewのDataContextにViewModelを設定する this.DataContext = new ViewModel(); } #endregion } }
MVVMのViewModel コード
ViewModel.cs
using System; using System.Collections.Generic; using System.ComponentModel; using System.Windows; using System.Windows.Input; using MVVMProtoType.Models; /////////////////////////////////////////////////////////////////////////////// // // WPF MVVM サンプル ViewModel コードです。 // // Copyright 2009 hiroxpepe // Author hiroxpepe <hiroxpepe@gmail.com> // Version 1.0.0 // Since 09.08.16 // Update 09.08.16 namespace MVVMProtoType.ViewModels { /// <summary> /// MVVM ViewModelクラスです。 /// </summary> public class ViewModel : INotifyPropertyChanged { /////////////////////////////////////////////////////////////////////// #region フィールド /// <summary> /// ユーザ名文字列を保存します。 /// </summary> private string userName; /// <summary> /// TimeLimeオブジェクトを格納したリストオブジェクトを保存します。 /// </summary> private List<TimeLine> timeLines; /// <summary> /// データ取得コマンドインターフェイスを保存します。 /// </summary> private ICommand selectCommand; /// <summary> /// Modelオブジェクトを保存します。 /// </summary> private Model model; #endregion /////////////////////////////////////////////////////////////////////// #region コンストラクタ /// <summary> /// 引数なしコンストラクタを実行します。 /// </summary> public ViewModel() { // コマンドインターフェイスの設定 this.selectCommand = new RelayCommand(this.selectCommand_Executed); // Modelオブジェクトの生成 this.model = new Model(); } #endregion /////////////////////////////////////////////////////////////////////// #region プロパティ /// <summary> /// データ取得コマンドを提供します。 /// </summary> public ICommand SelectCommand { get { return this.selectCommand; } } /// <summary> /// ユーザ名の文字列を提供します。 /// </summary> public string UserName { get { return this.userName; } set { if (this.userName == value) { return; } this.userName = value; // 更新の通知 this.NotifyPropertyChanged("UserName"); } } /// <summary> /// TimeLineを格納した、リストオブジェクトを提供します。 /// </summary> public List<TimeLine> DataGrid { get { return this.timeLines; } set { if (this.timeLines == value) { return; } this.timeLines = value; // 更新の通知 this.NotifyPropertyChanged("DataGrid"); } } #endregion /////////////////////////////////////////////////////////////////////// #region イベント /// <summary> /// 変更通知のイベントを実装します。 /// </summary> public event PropertyChangedEventHandler PropertyChanged; #endregion /////////////////////////////////////////////////////////////////////// #region プライベートメソッド /// <summary> /// 変更通知のイベントハンドラをコールします。 /// </summary> /// <param name="info">コントロールキー文字列が提供されます。</param> private void NotifyPropertyChanged(string info) { if (this.PropertyChanged != null) { this.PropertyChanged(this, new PropertyChangedEventArgs(info)); } } /// <summary> /// XMLデータを取得してリストデータを更新します。 /// </summary> /// <remarks> /// ここがコマンドのデリゲートから呼ばれます。 /// </remarks> /// <param name="sender">イベント送信オブジェクトが提供されます。</param> private void selectCommand_Executed(object sender) { try { // ViewModelのプロパティにModelから取得したデータを設定する this.DataGrid = this.model.GetDataContext(this.UserName); } catch (Exception err) { MessageBox.Show(err.Message); } } #endregion } }
RelayCommand.cs
using System; using System.Windows.Input; /////////////////////////////////////////////////////////////////////////////// // // WPF MVVM サンプル Command コードです。 // // Copyright 2009 hiroxpepe // Author hiroxpepe <hiroxpepe@gmail.com> // Version 1.0.0 // Since 09.08.16 // Update 09.08.16 namespace MVVMProtoType { /// <summary> /// MVVM 汎用のCommandクラスです。 /// </summary> public class RelayCommand : ICommand { /////////////////////////////////////////////////////////////////////// #region フィールド /// <summary> /// 汎用のvoid型デリゲートを保存します。 /// </summary> private Action<object> execute; /// <summary> /// 汎用のbool型デリゲートを保存します。 /// </summary> private Predicate<object> canExecute; #endregion /////////////////////////////////////////////////////////////////////// #region コンストラクタ /// <summary> /// コンストラクタを実行します。 /// </summary> /// <remarks> /// ※CanExecuteは常にtrueを返します。 /// </remarks> /// <param name="execute">コマンドのデリゲートが提供されます。</param> public RelayCommand(Action<object> execute) : this(execute, null) { } /// <summary> /// コンストラクタを実行します。 /// </summary> /// <param name="execute">コマンドのデリゲートが提供されます。</param> /// <param name="canExecute"> 実行の可否のデリゲートが提供されます。</param> public RelayCommand(Action<object> execute, Predicate<object> canExecute) { if (execute == null) { throw new ArgumentNullException("execute"); } this.execute = execute; this.canExecute = canExecute; } #endregion /////////////////////////////////////////////////////////////////////// #region パブリックメソッド /// <summary> /// コマンド実行可否を決定します。 /// </summary> public bool CanExecute(object parameter) { return (this.canExecute == null ? true : this.canExecute(parameter)); } /// <summary> /// コマンドを実行します。 /// </summary> public void Execute(object parameter) { this.execute(parameter); } #endregion /////////////////////////////////////////////////////////////////////// #region イベント /// <summary> /// ※ここでは使用されません。 /// </summary> public event EventHandler CanExecuteChanged; #endregion } }
MVVMのModel コード
Model.cs
using System; using System.Collections.Generic; using System.Net; using System.Text; using System.Xml; /////////////////////////////////////////////////////////////////////////////// // // WPF MVVM サンプル Model コードです。 // // Copyright 2009 hiroxpepe // Author hiroxpepe <hiroxpepe@gmail.com> // Version 1.0.0 // Since 09.08.16 // Update 09.08.16 namespace MVVMProtoType.Models { /// <summary> /// データ構造を表現するエンティティオブジェクトを実装します。 /// </summary> public class TimeLine { public string Date { get; set; } public string ID { get; set; } public string Client { get; set; } public string Text { get; set; } } /// <summary> /// MVVM Modelクラスです。 /// </summary> public class Model { /////////////////////////////////////////////////////////////////////// #region 定数 /// <summary> /// WEBサービス取得用のURLを定義します。 /// </summary> //const string serviceUrl = "{0}.xml"; const string serviceUrl = "http://twitter.com/statuses/user_timeline/{0}.xml"; #endregion /////////////////////////////////////////////////////////////////////// #region フィールド /// <summary> /// XMLデータを読み込んで処理する、XmlDocumentオブジクトを保存します。 /// </summary> private XmlDocument xmlDoc; /// <summary> /// データ部分をDOMノードリストとして処理する、XmlNodeListオブジクトを保存します。 /// </summary> private XmlNodeList xmlNodes; #endregion /////////////////////////////////////////////////////////////////////// #region コンストラクタ /// <summary> /// 引数なしコンストラクタを実行します。 /// </summary> public Model() { this.xmlDoc = new XmlDocument(); } #endregion /////////////////////////////////////////////////////////////////////// #region パブリックメソッド /// <summary> /// リストデータを取得します。 /// </summary> /// <param name="userName">ユーザ名が提供されます。</param> /// <returns>データオブジェクトを格納したリストを返します。</returns> public List<TimeLine> GetDataContext(string userName) { try { // XMLドキュメントを読み込む this.LoadXmlDoc(userName); // DOMノードを抽出する this.SelectDomNodes(); // リストデータを返す return this.CreateList(); } catch { // エラー処理してません・・ return null; } } #endregion /////////////////////////////////////////////////////////////////////// #region プライベートメソッド /// <summary> /// XMLドキュメントを読み込みます。 /// </summary> /// <param name="userName">ユーザ名が提供されます。</param> private void LoadXmlDoc(string userName) { this.xmlDoc.LoadXml(this.GetXmlDate(userName)); } /// <summary> /// XMLドキュメントからDOMノードを抽出します。 /// </summary> private void SelectDomNodes() { this.xmlNodes = this.xmlDoc.SelectNodes("//statuses/status"); } /// <summary> /// XMLノードをイテレーションしてリストデータを作成します。 /// </summary> /// <returns>データオブジェクトを格納したリストを返します。</returns> private List<TimeLine> CreateList() { var list = new List<TimeLine>(); foreach (XmlNode node in this.xmlNodes) { list.Add(new TimeLine { Date = this.FormatDate(node), ID = this.FormatId(node), Client = this.FormatClient(node), Text = this.FormatText(node) }); } return list; } /// <summary> /// サーバーからXMLデータを取得します。 /// </summary> /// <param name="userName">ユーザ名が提供されます。</param> /// <returns>取得したXMLデータ文字列を返します。</returns> private string GetXmlDate(string userName) { var timelineUrl = string.Format(serviceUrl, userName); WebClient webClient = new WebClient(); webClient.Encoding = Encoding.UTF8; byte[] data = webClient.DownloadData(timelineUrl); return Encoding.UTF8.GetString(data); } /// <summary> /// 日付をフォーマットします。 /// </summary> /// <param name="node">イテレーション中のNodeオブジェクトが提供されます。</param> /// <returns>書式変換された日付文字列を返します。</returns> private string FormatDate(XmlNode node) { var date = node["created_at"].InnerText; return DateTime.ParseExact(date, "ddd MMM dd HH':'mm':'ss zzzz yyyy", System.Globalization.DateTimeFormatInfo.InvariantInfo, System.Globalization.DateTimeStyles.None ).ToString("yyyy/MM/dd HH:mm:ss"); } /// <summary> /// 投稿IDをフォーマットします。 /// </summary> /// <param name="node">イテレーション中のNodeオブジェクトが提供されます。</param> /// <returns>書式変換された投稿ID文字列を返します。</returns> private string FormatId(XmlNode node) { return node["id"].InnerText; } /// <summary> /// クライアントをフォーマットします。 /// </summary> /// <param name="node">イテレーション中のNodeオブジェクトが提供されます。</param> /// <returns>書式変換されたクライアント文字列を返します。</returns> private string FormatClient(XmlNode node) { try { return node["source"].InnerText.Split('>')[1].Split('<')[0]; } catch { return node["source"].InnerText; } } /// <summary> /// テキスト文をフォーマットします。 /// </summary> /// <param name="node">イテレーション中のNodeオブジェクトが提供されます。</param> /// <returns>書式変換されたテキスト文字列を返します。</returns> private string FormatText(XmlNode node) { return node["text"].InnerText; } #endregion } }
思ったこと
- 前回のWPFサンプルの処理部分は、ほぼ全てModelの中に実装された、これは何を意味するのか?モデルはどのように表示されるかを知ってないといけないのでは? むぅ。