WPFでTwitterのタイムラインを表示する(MVVM版)

概要

前回のTwitterのユーザータイムラインをXMLデータで取得して、WPFのフォームグリッドに表示させるサンプルを、MVVMアーキテクチャでの実装でリファクタリングしてみた。

どうなった?

ViewとViewModelとModelを別々のコンポーネントで作成している、それぞれが下のレイヤーのコンポーネントに依存する形になっている。

以前から考えていた、プレゼンテーションコードの中でイテレーション処理されるリストオブジェクトが、MVVMではXAMLへのバインドで表現される。Microsoftはこうも見事に問題を解決しくれるのだ!考えが少し楽になった、やはり優れた設計概念に触れなければ進歩が無いC# いや.NET Frameworkでのアプリ実装は既にOOPの概念を超えている、OOPは実装のひとつの要素に過ぎないのだ。

イベントの通知コマンドに使うデリゲートを考えて見て欲しい、.NETでは、継承や委譲さえもまどろっこしいのだ。

MVVMのモデル図(下のレイヤーに依存する構造)


実行ファイル構成

  • View.exe
  • ViewModel.dll
  • Model.dll

※MVVMの実装はあくまでも主観的なものです、正解は有りません・・

MVVMのView部分 マークアップ

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
    }
}

思ったこと

  • ViewModelはModelのアダプター役だ、ModelはWPFのコアに依存していないのでWindows FormsやASP.NETにも転用出来るだろう。
  • 前回のWPFサンプルの処理部分は、ほぼ全てModelの中に実装された、これは何を意味するのか?モデルはどのように表示されるかを知ってないといけないのでは? むぅ。