C#Programing Archive
ここ一か月ほど、週末やらに暇を見つけては作っていたアプリケーションが完成しました。CardSafe/Eというタイトルで、いわゆるパスワードマネージャなのですが、特長としてはMesh enabled webアプリケーションとして実装していることが挙げられます。Mesh enabled webアプリケーションとは、Microsoftが開発中のLive Frameworkを利用したアプリケーションの一形態です。Mesh enabled webアプリケーションは、Live Meshという、MicrosoftによるDropBox的なデータ同期サービス上に配置され、複数デバイス・サーバー間でのアプリケーションとそのデータの同期が自動的に行われるという特長があり、開発に用いる技術がAjaxやSilverlightであることから、デスクトップアプリケーションとして動かすだけでなく、Webブラウザ上の通常のWebアプリケーションとしても動かすことが出来ます。CardSafe/Eはこれを利用し、データ同期機能を持ったパスワードマネージャを実現しています。
利用方法
残念なことに、このLive Frameworkという技術はまだCTP版という扱いであり、通常のLive Mesh Beta版ではMesh enabled webアプリケーションであるCardSafe/Eは利用できません。CardSafe/Eを利用するには、https://developer.mesh-ctp.com/という開発者用のLive Meshを利用する必要があります。開発者用Live Meshは、テスト用ということもあり、Mesh enabled webアプリが使えるほかは、Live Mesh Beta版に比べて機能がかなり省かれており、CardSafe/E共々常用には向かないかと思います。それでもLive Frameworkを触ってみたいという場合は、
- Microsoft ConnectのAzure Services Invitation ProgramからLive ServicesのInvitation Codeを取得
- そのInvitation Codeを利用してAzure Services Developer Portalに登録
- 開発者用Live Meshにサインイン
- 開発者用Live MeshのDeviceタブからLive Framework Clientをインストール
- MeshにCardSafe/Eをインストール←このURLを踏んでMeshにCardSafe/Eをインストール
という手順になります。開発者用Live Meshへの登録手順などは、使ってみよう! Live Framework:第3回 はじめようLive Framework CTP|gihyo.jp … 技術評論社という記事が非常に分かりやすいので是非参照して進めてください。
new CloudApp()
ところで。このアプリケーションはnew CloudApp()というMicrosoftのコンテストに出しています。気に入って頂けた場合は、new CloudApp(): The Azure™ Services Platform Developer Challenge – Voteという投票ページでCardSafe/Eにご投票頂けると励みになります。CardSafe/Eに清き一票を!(ぇ
CardSafe/Eの公開ページ
追補エントリを書きました。こちらもあわせてご参照ください:
UnhandledExceptionイベントは、サブスレッドの例外も捕捉可能 – SharpLab.
先日C# .NETアプリケーション開発 徹底攻略という本を購入しました。C#での業務アプリケーション開発におけるノウハウをまとめた本で、プロファイリングの方法など、普段、趣味グラマをしていると考えない部分について色々と触れているので大変参考になる本なのですが、一点、マルチスレッド処理における例外処理についての記述でおかしなところがありました。
2章6節4項の、サブスレッドで発生した例外の捕捉の仕方についての部分です。まず、93~95ページで、集約例外ハンドラではサブスレッドで発生した例外が捕捉出来ない、という部分があります。このようなコードです。
Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using System.Threading;
namespace ThreadExceptionBehavior {
static class Program {
/// <summary>
/// アプリケーションのメイン エントリ ポイントです。
/// </summary>
[STAThread]
static void Main() {
Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
Thread.GetDomain().UnhandledException += new UnhandledExceptionEventHandler(Program_UnhandledException);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
static void Program_UnhandledException(object sender, UnhandledExceptionEventArgs e) {
Exception ex = e.ExceptionObject as Exception;
MessageBox.Show(ex.Message);
}
static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e) {
MessageBox.Show(e.Exception.Message);
}
}
}
Form1.cs
using System;
using System.Windows.Forms;
namespace ThreadExceptionBehavior {
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
}
private delegate void SampleDelegate();
private void button1_Click(object sender, EventArgs e) {
//非同期スレッドの実行
SampleDelegate a = new SampleDelegate(ThreadMethod);
a.BeginInvoke(null, null);
}
private void ThreadMethod() {
//意図的に起こした例外
throw new Exception("非同期スレッドで起こった例外です。");
}
}
}
ここは良いのです。
そして続いての96~97ページで例外をメインスレッドへの通知して捕捉出来るように書き換える方法が書かれているのですが、このようになっています。
Form1.cs
using System;
using System.Windows.Forms;
namespace ThreadExceptionBehavior {
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
}
private delegate void SampleDelegate();
private void button1_Click(object sender, EventArgs e) {
//非同期スレッドの実行
SampleDelegate a = new SampleDelegate(ThreadMethod);
a.BeginInvoke(null, null);
}
private void ThreadMethod() {
if (this.InvokeRequired) {
//メインスレッドに呼び戻して実行
this.BeginInvoke(new SampleDelegate(ThreadMethod));
return;
}
throw new Exception("非同期スレッドで起こった例外です。");
}
}
}
19~23行目が加わった部分です。FormのInvokeRequiredプロパティがtrueの場合、つまりメインスレッド(UIスレッド)で実行されていない場合は、非同期にUIスレッドで実行しなおすようになっているのですが、これではサブスレッドでの例外処理の仕方の説明になっていません。ボタンをクリックした後直ぐに処理が帰ってくるという意味では非同期ではありますが、結局UIスレッド上で、ThreadMethodメソッドは動いているわけで、ThreadMethodメソッドに重たい処理を書いた場合は、UIがブロックされてしまうので無意味です。
本来ならば、以下のように書くべきではないでしょうか?
using System;
using System.Windows.Forms;
namespace ThreadExceptionBehavior {
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
}
private delegate void SampleDelegate();
private void button1_Click(object sender, EventArgs e) {
//非同期スレッドの実行
SampleDelegate a = new SampleDelegate(ThreadMethod);
a.BeginInvoke(null, null);
}
private void ThreadMethod() {
try {
//何らかの重たい処理
throw new Exception("非同期スレッドで起こった例外です。");
}
catch(Exception e){
this.BeginInvoke((MethodInvoker)delegate() {
throw new ApplicationException("サブスレッドで例外が発生しました。", e);
});
}
}
}
}
これならば重たい処理によってUIがブロックされることもなく、またサブスレッドで発生した例外も例外集約ハンドラで集めることが可能です。
Windows API Code Pack for Microsoft .NET Framework (v0.85)さわってみた。
中身は
- Windows 7 ライブラリを含む Windows Shell 名前空間オブジェクト、既知のフォルダーと非ファイルシステム コンテナ
- Windows Vista と Windows 7 のタスクダイアログ
- WPF と Windows Forms での Windows 7 Explorer Browser Control のサポート
- シェル プロパティ システムのサポート
- Windows 7 タスクバー ジャンプリスト、アイコン オーバーレイ、プログレスバー用のヘルパー
- Windows Vista と Windows 7 コモン ファイル ダイアログのサポート(カスタム ファイル ダイアログを含む)
- Direct3D 11.0とDXGI 1.0/1.1 APIのサポート
- センサー プラットフォーム API
- 拡張言語サービス API
川西 裕幸のブログ : Windows API Code Pack for Microsoft .NET Framework (v0.85)
だそうです。以下では同梱のサンプル(DirectX以外)を動かしてみた際のメモ。
Explorer Browser Control
Explorerのコントロール。使い場所は色々ありそうですね。左がWPF版、右がWin Forms版。もちろんWPF版の主要なプロパティは依存関係プロパティになっていまして、Thumbnail Sizeというスライダーを動かせば、バインドされたサムネイルの大きさがスルスルと変わっていきました。良いですね。
Shell
KnownFoldersBrowser
既知のフォルダ?(knownFolders)の一覧を表示するデモ。一覧はMicrosoft.WindowsAPICodePack.Shell.KnownFoldersクラスで取得しています。knownFoldersはVistaで導入されたものらしい。また、standard foldersとは異なり、ベンダが追加(削除)することができるらしい。knownFoldersについて詳しくはMSDNのknownFoldersについてのページを参照のこと。
PropertyEdit
ファイルのプロパティを操作するデモ
ShellHierarchyTreeDemo
階層ツリーのデモ
ShellObjectCFDBrowser
CommonFileDialogのデモ。表示する場所の初期値として設定できるのはフォルダだけではないのが特徴。Windows 7で追加された「ライブラリ」を初期値として設定出来たり、検索条件を初期値として指定して開くことが出来ます。これはちょっと感動ものでした。二枚目のSSは検索条件として「最近使ったファイル」という条件を付加して開いたCommonFileDialog。
StarBackUp
いまいちよくわからない。フォルダ選択ダイアログのサンプル?
StockIconsDemo
システムのアイコンを取得するのに使えるMicrosoft.WindowsAPICodePack.StockIconsクラスのデモ
TaskBarDemo
Windows7でのタスクバーでの通知を活用するためのデモ。タスクバー上でのプログレス表示や、ジャンプリストの作成のデモなど。
ThumbnailBrowserDemo
選択された要素のサムネイルをMicrosoft.WindowsAPICodePack.Shell.ShellObjectクラスのThumbnailプロパティから取得してPictureBoxに描画するサンプル。
センサー プラットフォーム API
サンプル動かそうとしたら落ちた。まぁ対応するセンサデバイスがないわけだから当然ですね。
拡張言語サービス API
多言語ユーザー インターフェイスのサポートと言語サービス
Windows 7 は、多言語ユーザー インターフェイスのサポートを強化し、アプリケーションから言語サービスを利用できるようにすることで、開発者がスタンダードな手法で国際市場に向けたアプリケーションを作成できるようになっています。
Extended Linguistic Services は、小規模の同じAPI 群を使用して、さまざまな先進の言語機能を有効活用できる、Windows 7 の新機能です。開発者は Extended Linguistic Services の API を使用することにより、任意の Unicode テキストから言語を自動検出し、その情報を元にして適切な言語が選択されるような、世界中のユーザーが快適に利用できるしくみを構築できます。 Extended Linguistic Services には、テキストの書記体系を変換する、組み込みの表記変換機能もサポートされています。たとえば、簡体字中国語と繁体字中国語間でテキストを自動的に変換すれば、言語の境界を越えた相互コミュニケーションを促進することが可能です。 Extended Linguistic Services の API を使用することにより、開発者は、既存の Extended Linguistic Services に加え、将来、新しいサービスが公開されたときには、新たにコードを覚え直すことなく、必要なサービスを選ぶだけで済むようになります。
この機能を利用するためのものみたい。あんまり弄れてないです。
WPF Futuresには、WPF Model-View-ViewModel Toolkitの他にも色々な参考になるサンプルが含まれています。Southridge Hands-on-LabはWPF Ribbon ControlとDataGridを使ったリッチなアプリケーション開発のチュートリアルとなっており、PDCのセッションでも出てきていたものがチュートリアルの形式としてまとめられた物のようですね。結構大きなアプリケーションですが、手取り足取り細かく解説してくれるので、参考になるところが多いのではないでしょうか。
ただ、M-V-VMパターンには則っていないところがあり、ViewModelに書くべきロジックをViewのコードビハインドに書いていたりするのでM-V-VMパターンのサンプルとするには注意が必要です。WPF Ribbon Controlのメニュー等は、CommandとしてExecuteメソッドやCanExecuteメソッド以外にも、LabelTitleプロパティなどを持ったオブジェクトを必要とするため、View側でCommandとしてセットするオブジェクトを用意する必要があります。その制約に引きずられてViewのコードビハインドにCommandの処理ロジックを記述しているようです。
では、Ribbon Controlを使用する場合はM-V-VMパターンが使えないかというとそういう訳でもなく、SouthridgeをM-V-VMパターンに則って書き直したバージョンも存在しているようです。自分は詳しく調べられていないのですが、先ほどの問題はNonRoutedRibbonCommandDelegatorというRibbonCommandと同じインターフェイスを備えたプロキシクラスをはさむことで解決しているようです。
・・・とここまで書いたところで気づいたのですが、Ribbon V1 Roadmapをみたところ、Ribbon関係のCommandに設計変更が入るみたいですね。LabelTitleとかまでRibbonCommandに纏めるのをやめて、通常のコントロールと同じようなスタイルに戻るようです。これは素直に新しいバージョンがリリースされるのを待ったほうが楽かもしれません。
WPF Model-View-ViewModel Toolkitのテンプレートやサンプルに含まれるDelegateCommand.cs、とりあえずコメントを日本語訳しながら読んでみました。DelegateCommandという名前ですが、Composite Application Guidance for WPFのDelegateCommandとは異なります。MSDN MagazineのWPF のための MODEL-VIEW-VIEWMODEL (MVVM) デザイン パターンという記事で使われているRelayCommandに、CommandManagerのRequerySuggestedイベントへの応答の有効/無効の設定プロパティ(IsAutomaticRequeryDisabled)と、CanExecuteChangedイベントを発生させるパブリックメソッド(RaiseCanExecuteChanged)を追加し、メモリリークを防ぐためにCanExecuteChangedイベントへのハンドラを弱参照で持つようにした?感じですね。
// This source code is licensed by Microsoft under Ms-PL license.
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Input;
namespace WpfModelViewApplication1.Commands
{
/// <summary>
/// <para>
/// This class allows delegating the commanding logic to methods passed as parameters,
/// and enables a View to bind commands to objects that are not part of the element tree.
/// </para>
/// <para>
/// このクラスはパラメータとして渡されたメソッドへのコマンドのロジックの委譲を実現します。
/// また、Viewが要素ツリーに含まれないオブジェクトにコマンドをバインドすることを可能にします。
/// </para>
/// </summary>
public class DelegateCommand : ICommand
{
#region Constructors
/// <summary>
/// Constructor
/// </summary>
public DelegateCommand(Action executeMethod)
: this(executeMethod, null, false)
{
}
/// <summary>
/// Constructor
/// </summary>
public DelegateCommand(Action executeMethod, Func<bool> canExecuteMethod)
: this(executeMethod, canExecuteMethod, false)
{
}
/// <summary>
/// Constructor
/// </summary>
public DelegateCommand(Action executeMethod, Func<bool> canExecuteMethod, bool isAutomaticRequeryDisabled)
{
if (executeMethod == null)
{
throw new ArgumentNullException("executeMethod");
}
_executeMethod = executeMethod;
_canExecuteMethod = canExecuteMethod;
_isAutomaticRequeryDisabled = isAutomaticRequeryDisabled;
}
#endregion
#region Public Methods
/// <summary>
/// <para>
/// Method to determine if the command can be executed
/// </para>
/// <para>
/// コマンドが実行可能かを返す
/// </para>
/// </summary>
public bool CanExecute()
{
if (_canExecuteMethod != null)
{
return _canExecuteMethod();
}
return true;
}
/// <summary>
/// <para>
/// Execution of the command
/// </para>
/// <para>
/// コマンドを実行する
/// </para>
/// </summary>
public void Execute()
{
if (_executeMethod != null)
{
_executeMethod();
}
}
/// <summary>
/// <para>
/// Property to enable or disable CommandManager's automatic requery on this command
/// </para>
/// <para>
/// CommandManagerのこのコマンドに対する自動再要求の有効/無効を設定する
/// </para>
/// </summary>
public bool IsAutomaticRequeryDisabled
{
get
{
return _isAutomaticRequeryDisabled;
}
set
{
if (_isAutomaticRequeryDisabled != value)
{
if (value)
{
CommandManagerHelper.RemoveHandlersFromRequerySuggested(_canExecuteChangedHandlers);
}
else
{
CommandManagerHelper.AddHandlersToRequerySuggested(_canExecuteChangedHandlers);
}
_isAutomaticRequeryDisabled = value;
}
}
}
/// <summary>
/// <para>
/// Raises the CanExecuteChaged event
/// </para>
/// <para>
/// CanExecuteChangedイベントを発生させる
/// </para>
/// </summary>
public void RaiseCanExecuteChanged()
{
OnCanExecuteChanged();
}
/// <summary>
/// <para>
/// Protected virtual method to raise CanExecuteChanged event
/// </para>
/// <para>
/// CanExecuteChangedイベントを発生させるprotected virtualメソッド
/// </para>
/// </summary>
protected virtual void OnCanExecuteChanged()
{
CommandManagerHelper.CallWeakReferenceHandlers(_canExecuteChangedHandlers);
}
#endregion
#region ICommand Members
/// <summary>
/// <para>
/// ICommand.CanExecuteChanged implementation
/// </para>
/// <para>
/// ICommand.CanExecuteChangedの実装
/// </para>
/// </summary>
public event EventHandler CanExecuteChanged
{
add
{
if (!_isAutomaticRequeryDisabled)
{
CommandManager.RequerySuggested += value;
}
CommandManagerHelper.AddWeakReferenceHandler(ref _canExecuteChangedHandlers, value, 2);
}
remove
{
if (!_isAutomaticRequeryDisabled)
{
CommandManager.RequerySuggested -= value;
}
CommandManagerHelper.RemoveWeakReferenceHandler(_canExecuteChangedHandlers, value);
}
}
bool ICommand.CanExecute(object parameter)
{
return CanExecute();
}
void ICommand.Execute(object parameter)
{
Execute();
}
#endregion
#region Data
private readonly Action _executeMethod = null;
private readonly Func<bool> _canExecuteMethod = null;
private bool _isAutomaticRequeryDisabled = false;
private List<WeakReference> _canExecuteChangedHandlers;
#endregion
}
/// <summary>
/// <para>
/// This class allows delegating the commanding logic to methods passed as parameters,
/// and enables a View to bind commands to objects that are not part of the element tree.
/// </para>
/// <para>
/// このクラスはパラメータとして渡されたメソッドへのコマンドのロジックの委譲を実現します。
/// また、Viewが要素ツリーに含まれないオブジェクトにコマンドをバインドすることを可能にします。
/// </para>
/// </summary>
/// <typeparam name="T">Type of the parameter passed to the delegates</typeparam>
public class DelegateCommand<T> : ICommand
{
#region Constructors
/// <summary>
/// Constructor
/// </summary>
public DelegateCommand(Action<T> executeMethod)
: this(executeMethod, null, false)
{
}
/// <summary>
/// Constructor
/// </summary>
public DelegateCommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod)
: this(executeMethod, canExecuteMethod, false)
{
}
/// <summary>
/// Constructor
/// </summary>
public DelegateCommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod, bool isAutomaticRequeryDisabled)
{
if (executeMethod == null)
{
throw new ArgumentNullException("executeMethod");
}
_executeMethod = executeMethod;
_canExecuteMethod = canExecuteMethod;
_isAutomaticRequeryDisabled = isAutomaticRequeryDisabled;
}
#endregion
#region Public Methods
/// <summary>
/// <para>
/// Method to determine if the command can be executed
/// </para>
/// <para>
/// コマンドが実行可能かを返す
/// </para>
/// </summary>
public bool CanExecute(T parameter)
{
if (_canExecuteMethod != null)
{
return _canExecuteMethod(parameter);
}
return true;
}
/// <summary>
/// <para>
/// Execution of the command
/// </para>
/// <para>
/// コマンドを実行する
/// </para>
/// </summary>
public void Execute(T parameter)
{
if (_executeMethod != null)
{
_executeMethod(parameter);
}
}
/// <summary>
/// <para>
/// Raises the CanExecuteChaged event
/// </para>
/// <para>
/// CanExecuteChangedイベントを発生させる
/// </para>
/// </summary>
public void RaiseCanExecuteChanged()
{
OnCanExecuteChanged();
}
/// <summary>
/// <para>
/// Protected virtual method to raise CanExecuteChanged event
/// </para>
/// <para>
/// CanExecuteChangedイベントを発生させるprotected virtualメソッド
/// </para>
/// </summary>
protected virtual void OnCanExecuteChanged()
{
CommandManagerHelper.CallWeakReferenceHandlers(_canExecuteChangedHandlers);
}
/// <summary>
/// <para>
/// Property to enable or disable CommandManager's automatic requery on this command
/// </para>
/// <para>
/// CommandManagerのこのコマンドに対する自動再要求の有効/無効を設定する
/// </para>
/// </summary>
public bool IsAutomaticRequeryDisabled
{
get
{
return _isAutomaticRequeryDisabled;
}
set
{
if (_isAutomaticRequeryDisabled != value)
{
if (value)
{
CommandManagerHelper.RemoveHandlersFromRequerySuggested(_canExecuteChangedHandlers);
}
else
{
CommandManagerHelper.AddHandlersToRequerySuggested(_canExecuteChangedHandlers);
}
_isAutomaticRequeryDisabled = value;
}
}
}
#endregion
#region ICommand Members
/// <summary>
/// <para>
/// ICommand.CanExecuteChanged implementation
/// </para>
/// <para>
/// ICommand.CanExecuteChangedの実装
/// </para>
/// </summary>
public event EventHandler CanExecuteChanged
{
add
{
if (!_isAutomaticRequeryDisabled)
{
CommandManager.RequerySuggested += value;
}
CommandManagerHelper.AddWeakReferenceHandler(ref _canExecuteChangedHandlers, value, 2);
}
remove
{
if (!_isAutomaticRequeryDisabled)
{
CommandManager.RequerySuggested -= value;
}
CommandManagerHelper.RemoveWeakReferenceHandler(_canExecuteChangedHandlers, value);
}
}
bool ICommand.CanExecute(object parameter)
{
// if T is of value type and the parameter is not
// set yet, then return false if CanExecute delegate
// exists, else return true
// Tが値型でかつ、parameterがセットされていない場合、
// CanExecuteデリゲートが存在するならfalseを返します。
// そうでなければtrueを返します。
if (parameter == null &&
typeof(T).IsValueType)
{
return (_canExecuteMethod == null);
}
return CanExecute((T)parameter);
}
void ICommand.Execute(object parameter)
{
Execute((T)parameter);
}
#endregion
#region Data
private readonly Action<T> _executeMethod = null;
private readonly Func<T, bool> _canExecuteMethod = null;
private bool _isAutomaticRequeryDisabled = false;
private List<WeakReference> _canExecuteChangedHandlers;
#endregion
}
/// <summary>
/// <para>
/// This class contains methods for the CommandManager that help avoid memory leaks by
/// using weak references.
/// </para>
/// <para>
/// このクラスはCommandManagerに対する、弱参照を使うことでメモリリークを防ぐ助けとなるメソッドを含んでいます。
/// </para>
/// </summary>
internal class CommandManagerHelper
{
internal static void CallWeakReferenceHandlers(List<WeakReference> handlers)
{
if (handlers != null)
{
// Take a snapshot of the handlers before we call out to them since the handlers
// could cause the array to me modified while we are reading it.
// 我々が私への配列を読んでいる間にハンドラはそれの変更を引き起こしうるので、
// それらを呼び出す前にハンドラのスナップショットをとります。
EventHandler[] callees = new EventHandler[handlers.Count];
int count = 0;
for (int i = handlers.Count - 1; i >= 0; i--)
{
WeakReference reference = handlers[i];
EventHandler handler = reference.Target as EventHandler;
if (handler == null)
{
// Clean up old handlers that have been collected
// 収集されたハンドラを削除します
handlers.RemoveAt(i);
}
else
{
callees[count] = handler;
count++;
}
}
// Call the handlers that we snapshotted
// スナップショットをとったハンドラを呼びます
for (int i = 0; i < count; i++)
{
EventHandler handler = callees[i];
handler(null, EventArgs.Empty);
}
}
}
internal static void AddHandlersToRequerySuggested(List<WeakReference> handlers)
{
if (handlers != null)
{
foreach (WeakReference handlerRef in handlers)
{
EventHandler handler = handlerRef.Target as EventHandler;
if (handler != null)
{
CommandManager.RequerySuggested += handler;
}
}
}
}
internal static void RemoveHandlersFromRequerySuggested(List<WeakReference> handlers)
{
if (handlers != null)
{
foreach (WeakReference handlerRef in handlers)
{
EventHandler handler = handlerRef.Target as EventHandler;
if (handler != null)
{
CommandManager.RequerySuggested -= handler;
}
}
}
}
internal static void AddWeakReferenceHandler(ref List<WeakReference> handlers, EventHandler handler)
{
AddWeakReferenceHandler(ref handlers, handler, -1);
}
internal static void AddWeakReferenceHandler(ref List<WeakReference> handlers, EventHandler handler, int defaultListSize)
{
if (handlers == null)
{
handlers = (defaultListSize > 0 ? new List<WeakReference>(defaultListSize) : new List<WeakReference>());
}
handlers.Add(new WeakReference(handler));
}
internal static void RemoveWeakReferenceHandler(List<WeakReference> handlers, EventHandler handler)
{
if (handlers != null)
{
for (int i = handlers.Count - 1; i >= 0; i--)
{
WeakReference reference = handlers[i];
EventHandler existingHandler = reference.Target as EventHandler;
if ((existingHandler == null) || (existingHandler == handler))
{
// Clean up old handlers that have been collected
// in addition to the handler that is to be removed.
// 削除されるハンドラに加えて、収集された古いハンドラ
// を削除します。
handlers.RemoveAt(i);
}
}
}
}
}
}
5/1にCodePlexのWPFのサイトでリリースされたWPF Futures、様々なサンプルやテンプレートが含まれていて興味深いのですが、この中には、WPF Model-View-ViewModel Toolkitというものも含まれていました。
内容は、
- Visual Studio 2008用のテンプレート (Visual C# Express 2008もサポート)
- 文書
- M-V-VMの概略紹介
- VSテンプレート利用の手引き
- MVVMパターンの適用されたWPFアプリケーションのデモ
となっています。
Visual Studioテンプレート
規約というほどのものではありませんが、M-V-VMパターンもRuby on RailsやASP.NET MVC、CakePHPなどのMVCパターンのフレームワークのように命名規則やディレクトリ構造の作法が存在します。Model、View、ViewModel毎にディレクトリを作成し、その中にModelやViewといった接尾辞をつけたクラスを配置するといったものです。この作法に則ったプロジェクトを簡単に作成するために、WPF Model-View-ViewModel ToolkitにはVisual Studioのテンプレートが含まれています。
テンプレートのインストール
ダウンロードしてきたToolkitのzipファイルの中には、WPFModelViewTemplate.msiが含まれています。まずはこれを実行してインストールを行います。
ただし、Visual Studioの日本語環境では、これだけでは利用可能になりません。ASP.NET MVCのPreview版がそうであったように、Visual Studio のテンプレート が英語環境用(1033)にしかセットアップされませんので、英語環境用にインストールされたファイルを日本語環境用(1041)にコピーしてやる必要があります。
以下の位置に存在するファイルを、
<Visual Studioのインストールディレクトリ>\Common7\IDE\ProjectTemplates\CSharp\Windows\1033\WpfModelViewApplicationProjectTemplateV0.1.cs.zip
こちらのディレクトリにコピーします。
<Visual Studioのインストールディレクトリ>\Common7\IDE\ProjectTemplates\CSharp\Windows\1041\
その上で、管理者として実行したVisual Studioコマンドプロンプトから
devenv /InstallVSTemplates
を実行し、VisualStudioに変更を反映させます。
これで、テンプレートが追加されました。
CommandやViewModelの基本クラスも用意されています。CommandやViewModelの基本クラスは様々な人が公開しているものの中からどれを選択すべきか迷うところがありましたが、これからWPF Model-View-ViewModel Toolkitが標準として育っていけば、迷うことも少なくなりそうですね。
DelegateCommandやViewModelBaseについては、追って解説エントリをあげられればと思います。
【翻訳】ViewとViewModel間でアニメーション化された遷移を仕立てる~ 原題”Orchestrating Animated Transitions between View and ViewModel”
M-V-VMパターンとアニメーションをどのようにして組み合わせるのかは、MVVMパターンを使う上で常に頭を悩ませる問題です。この問題に対して、Josh SmithさんがOrchestrating Animated Transitions between View and ViewModel >> Josh Smith on WPFというエントリでひとつの方法を示していたので、今回はこれを訳してみました。
Introduction
Model-View-ViewModel(MVVM)パターンを使う場合、Viewに表示されるデータはほぼ常にデータバインディングを通じてやり取りされます。ViewModelオブジェクトのプロパティに対する変更はすぐにViewのバインディングターゲットに対して伝達されます。これは普通は良いことです。しかしながら、時によっては、伝達が即時に行われることが問題になることがあります。
When working with the Model-View-ViewModel (MVVM) pattern, the data shown in a View is almost always transferred to it via data binding. Changes made to properties on a ViewModel object immediately propagate to the binding targets in the View. This is normally a good thing. Sometimes, however, the immediacy of the propagation can be a problem.
なぜ新しいプロパティの値がUIに即座には現われて欲しくないと思うのでしょうか?では、古い値と新しい値との間でのアニメーション化された遷移を表示したい場合を考えてみてください。新しい値が即座にViewに表示された場合、新しい値を”アニメートイン”する前に古い値を”アニメートアウト”するチャンスがありません。例えば、プロパティの値がリストとして表示される1ページのデータ項目だと考えてみてください。新しい項目のページをフェードする前に最初のページのアイテムをフェードアウトしたい場合、新しい項目のページが表示される前に、フェードアウトアニメーションを始める必要があります。
Why would you want a new property value to not immediately show up in the UI? Well, suppose you want to display an animated transition between the old value and the new value. If the new value immediately shows up in the View, you won’t have a chance to animate “out” the old value before animating “in” the new value. For example, suppose the property’s value is a page of data items to show in a list. If we want to fade away the first page of items before fading in the new page of items, we need to start the fade-away animations before the new page of items is displayed.
これは、Visual State Manager(VSM)が解決する種類の問題です。VSMは未だ正式なWPFの一部になっておらず(.NET 4.0まではなりません)、我々は未だそれをアプリケーション作成に役立てることはできません。また、VSMはカスタムコントロールの中で使われるために設計されているので、私達がこれから探そうというものとは完全に同じという訳ではありません。
This is the type of problem that the Visual State Manager (VSM) solves. Since the VSM is not yet part of WPF proper (it won’t be until .NET 4.0), we cannot make use of it yet in production applications. Also, the VSM is designed to be used inside of custom controls, so it is not exactly equivalent to what we are going to explore below.
The AnimatedTransition Class
私はWPFやSilverlightのアプリケーションに取り組んでいる際に、まさにこの問題に何回も直面しました。プロパティの変化をアニメーション化された遷移と協調させるための様々なアプローチを試したあと、私は核となる問題を、より鮮明に理解し始めました。昨日私はScherzo from Mendelssohn’s “A Midsummer’s Night Dream”のこのとてつもなく素晴らしい演奏を聴き、そしてパッと閃きました。私は二つのプロパティ値間でのアニメーション化された遷移を作成するための、ViewModelとViewをして協調させるシンプルで、タイプセーフで、再利用可能な方法を悟りました。AnimatedTransitionクラスが生まれたのです!
I faced this exact problem many times while working on WPF and Silverlight applications. After experimenting with several approaches to coordinating property changes with animated transitions, I started to understand the core problem more clearly. Yesterday I listened to this incredible rendition of the Scherzo from Mendelssohn’s “A Midsummer’s Night Dream” and was hit by a flash of inspiration. I realized a way to create a simple, type-safe, reusable way of having a ViewModel and View work together to create animated transitions between two property values. The AnimatedTransition class was born!
AnimatedTransitionのインスタンスはどのViewModelクラスにでも埋め込むことが出来ます。遷移を行う必要のあるViewは、遷移を何時始めるべきかを知るために、AnimatedTransitionオブジェクトにアクセスし、そのイベントの一つ以上をフックする必要があります。
An instance of AnimatedTransition can be embedded in any ViewModel class. The View(s) that need to perform transitions must access the AnimatedTransition object and hook one or more of its events in order to know when transitions should start.
The Demo App
このエントリの最後に載せたデモアプリでは、以下のように「ページ」毎に四つのハローキティを表示するItemsControlがあります。
In the demo app, which is available at the bottom of this post, there is an ItemsControl that displays four Hello Kitty images per “page,” as seen below…
ユーザ―が下の四角を通じて別のページに遷移する際に、古いキティがフェードアウトし、新しいのがフェードインします。
When the user navigates to a different page, via the circles below, the old kitties fade away and then the new ones fade in.
この小さいアプリを動かすのに関係している全てのクラスを精査していくことはしません。ViewModelクラスについてしっかりと理解したい場合は、このクラスダイアグラムをクリックしてフルサイズのを見てください。
We are not going to examine all of the classes involved in making this little app work. If you want to get a high-level understanding of the ViewModel classes, click on this class diagram to view it at full size:
ItemsControlに表示されるキティの現在のページはKittyKatTerraceViewModelのSelectedPageプロパティに含まれます。あのプロパティの二つの値の間での遷移を必要とする場合、我々はAnimatedTransitionを埋め込んで使います。KittyKatTerraceViewModelがAnimatedTransitionを使用し、公開する方法はこのようになっています:
The current page of kitties to show in the ItemsControl is contained in SelectedPage property of KittyKatTerraceViewModel. Since we need to have a transition between the two values of that property, we use an AnimatedTransition to enable that. Here is how KittyKatTerraceViewModel uses and exposes an AnimatedTransition:
public AnimatedTransition<PageOfKittyKatsViewModel> GetSelectedPageTransition()
{
if (_selectedPageTransition == null)
_selectedPageTransition =
new AnimatedTransition<PageOfKittyKatsViewModel>(page => this.SelectedPage = page);
return _selectedPageTransition;
}
public PageOfKittyKatsViewModel SelectedPage
{
get { return _selectedPage; }
private set
{
if (value == _selectedPage)
return;
_selectedPage = value;
foreach (var page in this.Pages)
page.RefreshSelectionState();
this.OnPropertyChanged("SelectedPage");
}
}
ユーザーがページを選択するためにItemsControlの真下の小さい四角をクリックしたとき、PageOfKittyKatsViewModelクラスのこのメソッドが実行されます:
When the user clicks on a little circle beneath the ItemsControl to select a page, this method in the PageOfKittyKatsViewModel class executes:
void Select()
{
// Tell the KittyKatTerraceViewModel to begin
// a transition of the SelectedPage property
// so that it references this page.
_owner.GetSelectedPageTransition().Start(this);
}
KittyKatTerraceViewコントロールのコードビハインドにある全ての関連するロジックを以下にリストアップします。これはViewがどうやってAnimatedTransitionオブジェクトを使用し、相互作用しているかを示しています。
All of the relevant logic in the KittyKatTerraceView control’s code-behind is listed below. This shows how a View can consume and interact with an AnimatedTransition object.
void OnItemsHostPanelLoaded(object sender, RoutedEventArgs e)
{
_itemsHost = sender as Panel;
}
void OnLoaded(object sender, RoutedEventArgs e)
{
var viewModel = base.DataContext as KittyKatTerraceViewModel;
viewModel.SetKittyLocations(new List<Point>
{
new Point(255, 65),
new Point(115, 145),
new Point(280, 225),
new Point(135, 340)
});
_transition = viewModel.GetSelectedPageTransition();
_transition.BeforeApplyNewValue += this.OnBeforeNewSelectedPage;
_transition.AfterApplyNewValue += this.OnAfterNewSelectedPage;
}
void OnBeforeNewSelectedPage(object sender, EventArgs e)
{
Storyboard sb = this.CreateStoryboard("FadeOutAnim");
sb.Completed += delegate { _transition.Finish(); };
sb.Begin();
}
void OnAfterNewSelectedPage(object sender, EventArgs e)
{
// Wait until the panel has new children before animating.
_itemsHost.LayoutUpdated += this.OnItemsHostLayoutUpdated;
}
void OnItemsHostLayoutUpdated(object sender, EventArgs e)
{
_itemsHost.LayoutUpdated -= this.OnItemsHostLayoutUpdated;
this.CreateStoryboard("FadeInAnim").Begin();
}
古いアイテムをフェードアウトさせるStoryboardのCompletedイベントにハンドラをアタッチする必要があることによく気を付けて下さい。そのイベントが発生したとき、新しい値への遷移を終わらせるために、AnimatedTransitionは発生を伝えられる必要があり、それをうけて実際にその値を設定します。それはFinish()メソッドを呼ぶことによって実現されます。一度新しい値が設定されたならば、AfterApplyNewValueイベントが発生し、その際あなたは新しいアイテムにフェードインすることが出来ます。
It’s important to note that you must attach a handler to the Completed event of the Storyboard that fades away the old items. When the event is raised, the AnimatedTransition must be told to finish transitioning to the new value, and actually apply the value. That is accomplished by calling the Finish() method. Once the new value has been applied, the AfterApplyNewValue event is raised, at which time you can fade in the new items.
ソースコードはここからダウンロードすることが出来ます。注意:ファイルの拡張子を.DOCから.ZIPに変更して解凍して下さい。
You can download the source code here. Note: Be sure to change the file extension from .DOC to .ZIP and then decompress it.
