プロパティ変更とエラー情報の通知 (概念編)

先にプロパティ変更とエラー情報の通知 (実装編) を投稿しましたが、
今回は通知のプログラミング手法について概念的に考えてみます。

まず、従来のいわゆる手続き的なプログラミング手法の場合で、次のようなコードがあったとします。

try
{
    // ある処理を実行する

    // ある処理の実行が完了した場合の処理
}
catch (Exception) // エラーが発生する
{
    // エラーが発生した場合の処理
}

この手続きの中に現れる処理を、観測者と被観測者の 2 つのオブジェクトに役割を分担させるように変更します。
そのためには、被観測者側のコードを次のようにします。

try
{
    // ある処理を実行する

    // ある処理の実行が完了したことを観測者に通知する (通知後の処理は観測者が知っている)
}
catch (Exception) // エラーが発生する
{
    // エラーが発生したことを観測者に通知する (通知後の処理は観測者が知っている)
}

ある処理を被観測者側で実行し、それが完了したら観測者に通知します。
観測者はその通知をトリガーとして何らかの処理を実行します。

これが Observer パターンで、この Wikipedia に載っている図と同様にクラス図を作ると次のようになります。

Observer パターン

 

通知後の処理を関数として渡せるようにするには、プログラミング インターフェイスを次のように変形します。

Observer パターン

 

.NET の場合、イベントを利用することができます。

Observer パターン

 

.NET ではとくに、データのエンティティなどに適用できるインターフェイスとして
INotifyPropertyChanged, INotifyDataErrorInfo が用意されており、
XAML 系テクノロジのデータ バインディングでは、これらを利用してプレゼンテーション層とモデル層における関心事を分離します。
つまり、アプリの本質的なロジックをモデル層に集中させ、プレゼンテーション層は同期的に追従して UI を更新する、
といったプログラミング スタイルを実現できます。

なお、プログラミング インターフェイスの変更については、
Reactive Extensionsの概要と利用方法にも同様の議論が記載されています。

参照
プロパティ変更とエラー情報の通知 (実装編)
Observer パターン
Reactive Extensionsの概要と利用方法

プロパティ変更とエラー情報の通知 (実装編)

XAML 系テクノロジにおけるデータ バインディングは、
プレゼンテーション層とモデル層の間のデータ同期を自動化するプログラミング モデルを実現するための仕組みです。
バインディング ソース (主にモデル層のデータ) では、オブジェクト内で変化が起こった場合にそのことを通知します。

これは Observer パターンの活用例の 1 つです。
Observer パターンについては、プロパティ変更とエラー情報の通知 (概念編) に書きました。

以下では、主に MSDN ライブラリの資料をもとに、バインディング ソースを実装する際の注意点をまとめました。
大きく、プロパティ変更通知とエラー情報通知に分けられます。

 

■ プロパティ変更通知

オブジェクトのプロパティの値が変更される・されたことを通知するには、
INotifyPropertyChanging インターフェイス (プロパティ変更前) および
INotifyPropertyChanged インターフェイス (プロパティ変更後) を実装します。
INotifyPropertyChanging インターフェイスの実装は省略されることもあります。

これらのインターフェイスでは PropertyChanging イベントおよび PropertyChanged イベントを実装することになりますが、
イベント引数の PropertyName には、変更されたプロパティの名前を指定します。
ここで null または空文字列を指定すれば、オブジェクトのすべてのプロパティが変更されたという意味になります。
ただし、実際には null はあまり使わず、空文字列を使うことが多いようです。

以前は、プロパティ内でこれを実装するためにプロパティ名の文字列をコードに埋め込んでいましたが、
.NET Framework 4.5 または Windows ストアでは CallerMemberName 属性を利用することで回避できます。

public class Person : INotifyPropertyChanged
{
    private string name;

    public string Name
    {
        get { return name; }
        set
        {
            if (name == value) return;
            name = value;
            NotifyPropertyChanged();
        }
    }

    public event PropertyChangedEventHandler PropertyChanged = (o, e) => { };

    public void NotifyPropertyChanged([CallerMemberName]string propertyName = "")
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

 

⋄ コレクション変更通知

コレクションが変更されたことを通知するには、INotifyCollectionChanged インターフェイスを実装します。
自分で実装しなくても、ObservableCollection<T> クラスを利用すればほぼ十分です。

 

■ エラー情報通知

オブジェクトの中でエラーが発生したことを通知するには、INotifyDataErrorInfo インターフェイスを実装します。
主に、ユーザーからの入力値を検証する場合などに利用されます。
INotifyDataErrorInfo インターフェイスは Silverlight で先に登場しており、解説もこちらのほうが詳しく記載されています。

なお、以前から IDataErrorInfo インターフェイスがありましたが、
これは古い形式で、通知イベントが発生する形式にはなっていません。今後は使われなくなるでしょう。

さて、以下は INotifyDataErrorInfo インターフェイスの実装方法です。

内部でエラーが発生したら、例えば変数などにエラー情報をキャッシュしておき、ErrorsChanged イベントを発生させます。
プロパティ変更通知のときと同様、ErrorsChanged イベントの引数にはプロパティ名を渡します。
ここで null または空文字列を指定すれば、オブジェクトのレベルでエラーが発生したという意味になります。

そして GetErrors メソッドで、実際のエラーのコレクションを取得できるようにします。

HasErrors プロパティでは、エンティティ レベルにもプロパティ レベルにもエラーが存在しない場合に false を返します。
ただし、標準のバインディング エンジンに利用されることはないようです。
通常は、プロパティ名を指定できる ErrorsChanged イベントと GetErrors メソッドを使います。

INotifyDataErrorInfo.ErrorsChanged イベントの説明には、ErrorsChanged イベントを UI スレッドで発生させる、
と書かれていますが、Binding オブジェクトでデータ バインディングを設定してある場合には
UI スレッドでデータ ソースの値が取得されるため、UI スレッドでなくてもとくに問題はなさそうです。

以下は、その他の注意点です。

・GetErrors メソッドで INotifyCollectionChanged が実装されたコレクションを返す場合であっても、
    ErrorsChanged イベントを発生させる。
・GetErrors の戻り値の各アイテムに対して ToString メソッドを呼び出したらエラー メッセージを取得できる。
    例えば、ValidationResult オブジェクトを利用する。

 

INotifyDataErrorInfo インターフェイスの実装方法については以上ですが、
標準のバインディング エンジンの中核である Binding オブジェクトの設定の注意についても記述しておきます。

・ValidatesOnNotifyDataErrors プロパティ : INotifyDataErrorInfo のエラー検知をする場合。
・ValidatesOnDataErrors プロパティ : IDataErrorInfo のエラー検知をする場合。通常は使いません。
・ValidatesOnExceptions プロパティ : 例外を検知する場合。
・NotifyOnValidationError プロパティ : エラーを検知したときに、WPF の場合は UI 要素の Validation.Error 添付イベントを、
    Silverlight の場合は UI 要素の BindingValidationError イベントを発生させる。

これらのプロパティの既定値は、ValidatesOnNotifyDataErrors のみ true で、その他はすべて false です。
Silverlight の ValidationSummary などの一部のコントロールを利用する場合、
NotifyOnValidationError プロパティを true にします。

 

プラットフォーム
.NET Framework
Silverlight
Windows Phone
Windows ストア
Portable Class Library

参照
プロパティ変更とエラー情報の通知 (概念編)
INotifyPropertyChanged インターフェイス
INotifyCollectionChanged インターフェイス
INotifyDataErrorInfo インターフェイス
弱いイベント パターン

Windows Phone で既存のデータベース ファイルを使う

(Windows Phone Advent Calendar 2011 の 18 日目です。)

Windows Phone 7.5 アプリケーションでは、ローカル データベースとして SQL Server Compact を利用できます。
データベース ファイルの拡張子は .sdf です。
アプリケーションから接続するには LINQ to SQL を利用します。

今回は、Database First で既存のデータベース ファイル (.sdf) からコードを自動生成し、
Windows Phone 7.5 アプリケーションで動作させるまでの一連の手順について紹介します。

ちなみに、MSDN ライブラリの Windows Phone のローカル データベースの概要を見ると、
Code First な方法は載っているのですが Database First な方法は載っていません。
なお、Code First な方法についての日本語の解説は、
はぢめてのWindows Phone 7でのデータベース(Linq to Sql) などを参照するとよいでしょう。

■ データベース ファイルを準備する

「既存のデータベース ファイル (.sdf)」と書きましたが、
これまでに Windows Phone プラットフォーム以外で使用されていたものや、新規に作成したものも含みます。

ただし、方法: Windows Phone アプリケーションと共に参照データベースを展開する
「重要な注」に記述されている通り、デスクトップで作成された SQL Server Compact データベースを
Windows Phone アプリケーションで動作させることは公式にサポートされていません

したがって、自己責任で利用しましょう。

以下に、SQL Server Compact のデータベース ファイルを新規に作成する方法を簡単に示します。

(1) SQL Server Management Studio を使う場合

[サーバーへの接続] ダイアログの [サーバーの種類] で SQL Server Compact を選択し、
[データベース ファイル] で <新しいデータベース…> を選択します。
すると別のダイアログが表示されるので、ファイル名を指定します。

SQL Server Management Studio でデータベース ファイルを作成

あとはこのデータベース ファイルに接続してテーブルを定義します。
ただし、SQL Server の場合とは異なり、データベース ダイアグラムは利用できません。

SQL Server Management Studio でデータを作成

(2) Visual Studio を使う場合

Windows Phone 系のプロジェクトからでは作成できません。
Windows アプリケーション系のプロジェクトから次の図のように作成します。

Visual Studio でデータベース ファイルを作成

あとはサーバー エクスプローラーからデータベース ファイルに接続してテーブルを定義します。
コンテキスト メニューの [テーブル データの表示] からデータをグリッド形式で操作することもできます。
SQL を利用することもできます。

Visual Studio でデータを作成

さて、初期データを外部から一括で取り込みたい場合についてですが、
SQL Server Compact データベースには bcp ユーティリティでデータを一括でインポートできません。
次のような方法が考えられます。

  • Integration Services を利用する (未検証)
  • 後述する Proxy コードを生成してからコンソール アプリケーションを作成してデータを投入する
  • INSERT 文を生成して Management Studio や Visual Studio で実行する

■ データベース ファイルからコードを生成する

SQL Server などのデータベースを対象とした開発では、
Visual Studio で既存のデータベースから Proxy クラスを自動生成するという方法がよく用いられています。
しかし残念ながら、Visual Studio には .sdf ファイルから LINQ to SQL 用のコードを自動生成する機能がありません。

そこで、SqlMetal.exe というコマンドライン ツールを利用します。
これを使うと、.sdf ファイルから LINQ to SQL 用のコードを自動生成できます。
SqlMetal.exe は Windows SDK に含まれています (Visual Studio とともにインストールされます)。

例えば、次に示すようなバッチ ファイルを作成します。
SqlMetal.exe の引数には、.sdf ファイルのパス、作成されるコード ファイルのパス、
型指定された DataContext の名前空間とクラス名を指定します。絶対パスおよび相対パスが使えます。

CreateDataContext.bat


rem 64 bit OS の場合
set SqlMetal="C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\SqlMetal.exe"
%SqlMetal% Test.sdf /code:TestDataContext.cs /namespace:PhoneApp1 /context:TestDataContext /pluralize


このバッチ ファイルを実行すると TestDataContext.cs が生成されます。
ただし、本来これは .NET Framework 用のコードであり、Windows Phone 用のプロジェクトでは
型指定された DataContext クラスの一部のコンストラクターがコンパイル エラーとなるので、それらを手動で削除します。

TestDataContext.cs


// ・・・ (省略) ・・・
public partial class TestDataContext : System.Data.Linq.DataContext
{
// ・・・ (省略) ・・・

    public TestDataContext(string connection) :
            base(connection, mappingSource)
    {
        OnCreated();
    }

/* IDbConnection は存在しません。
    public TestDataContext(System.Data.IDbConnection connection) :
            base(connection, mappingSource)
    {
        OnCreated();
    }
*/

    public TestDataContext(string connection, System.Data.Linq.Mapping.MappingSource mappingSource) :
            base(connection, mappingSource)
    {
        OnCreated();
    }

/* IDbConnection は存在しません。
    public TestDataContext(System.Data.IDbConnection connection, System.Data.Linq.Mapping.MappingSource mappingSource) :
            base(connection, mappingSource)
    {
        OnCreated();
    }
*/

    public System.Data.Linq.Table<Company> Companies
    {
        get
        {
            return this.GetTable<Company>();
        }
    }
        
    public System.Data.Linq.Table<Line> Lines
    {
        get
        {
            return this.GetTable<Line>();
        }
    }

// ・・・ (省略) ・・・


その他の注意点
(1) Visual Studio コマンド プロンプトを利用すれば、SqlMetal.exe の絶対パスは必要ありません。
(2) このコードは、同じテーブル構造を持った SQL Server などに接続する場合にも利用できます。
     コンストラクターの引数に渡す接続文字列を変えるだけです。
(3) 引数に /pluralize を付加すると、単語の単数形・複数形を適切に判断してコードを生成します。
     上記の例では、Companies テーブルのエンティティの名前が Company になっています。

■ データベース ファイルを配置する

上記で準備したデータベース ファイル (.sdf) とコード ファイル (.cs) を Windows Phone アプリケーションの
プロジェクトに追加します (自動的にデータベース ファイルの [ビルド アクション] がコンテンツ、
コード ファイルの [ビルド アクション] がコンパイルになるはずです)。
また、プロジェクトの参照設定に System.Data.Linq を追加します。

初回実行時にこのデータベース ファイルを分離ストレージに配置するために、
アプリケーションの Launching イベントの処理を次のように追加します。
なお、接続文字列が特殊なので注意が必要です。isostore:/ のあとに分離ストレージ内でのパスを指定します。

App.xaml.cs


    // ・・・ (省略) ・・・

    private static readonly Uri TestContentUri = new Uri("Test.sdf", UriKind.Relative);
    private const string TestStoragePath = "Test.sdf";
    private const string TestConnectionString = "Data Source=isostore:/Test.sdf";

    // (たとえば、[スタート] メニューから) アプリケーションが起動するときに実行されるコード
    // このコードは、アプリケーションが再アクティブ化済みの場合には実行されません
    private void Application_Launching(object sender, LaunchingEventArgs e)
    {
        using (var context = new TestDataContext(TestConnectionString))
        {
            if (!context.DatabaseExists())
            {
                using (var input = Application.GetResourceStream(TestContentUri).Stream)
                using (var storage = IsolatedStorageFile.GetUserStoreForApplication())
                using (var output = storage.CreateFile(TestStoragePath))
                {
                    input.CopyTo(output);
                }
            }
        }
    }

    // ・・・ (省略) ・・・


■ データベース ファイルに接続する

ここまでできれば、もう難しいことはありません。
データベースに接続してデータを操作するには LINQ to SQL を利用します。


    private const string TestConnectionString = "Data Source=isostore:/Test.sdf";

    public static Company[] GetCompanies()
    {
        using (var context = new TestDataContext(TestConnectionString))
        {
            return context.Companies.ToArray();
        }
    }


バージョン情報
Windows Phone OS 7.1
Visual Studio 2010 SP1

参照
Windows Phone のローカル データベースの概要 (MSDN)
方法: Windows Phone アプリケーションと共に参照データベースを展開する (MSDN)
SqlMetal.exe (コード生成ツール) (MSDN)
Using SqlMetal to generate Windows Phone Mango Local Database classes (WindowsPhoneGeek)
はぢめてのWindows Phone 7でのデータベース(Linq to Sql) (neue cc)
Local Database for Windows Phone (雑記 – otherwise)