前回の「MVVMを使用した場合のViewのフェードアウトの実装方法について」について書きなぐりだけど実装が一応出来たのであげます。
まず、MSDNマガジンの「Model-View-ViewModel デザイン パターンによる WPF アプリケーション」を元にしていますので、出てくる文言が分からない場合はリンク先を参照してください。

参考サイトでは、MainWindowView内でWorkspaceエリアにAllCustomerViewとCustomerViewが動的に配置されます。
MVVMパターンを使用しているので、Viewの配置はViewModelインスタンスの扱いによって行われます。
Viewが描画される仕組みは、ViewModelとViewがDataTemplateによって関連付けられ、ViewModelインスタンスが存在した場合にViewが読み込まれます。
逆にViewModelインスタンスが破棄されるとデータが有効でなくなるためViewも破棄されます。

DataTemplateの仕組みがある為、ViewModelはViewの描画に関しては知る必要が無く、Viewも自身のDataContextを元にデータバインドを行うためデータの管理に関して知る必要がありません。
この疎結合を生み出すパターンとしてMVVMが使用されます。

これを踏まえて、例えば動的に配置されるAllCustomerViewやCustomerViewが配置・破棄される際にフェードイン・フェードアウトなどのインタラクションを付け加えようとした場合にどのように実装すれば良いのでしょうか?
Viewの開始時のフェードインについてはUserControlのLoadedイベントにフェードイン用のアニメーションをイベントトリガにて設定すれば実装出来ます。

問題なのはView終了時のフェードアウトです。

Viewを終了する為にはViewModelインスタンスを破棄する必要があります。
アニメーションは設定対象のオブジェクト(View)のインスタンスが有効である期間しか行われない(見えない)為、Viewの終了時のフェードアウトを行っている場合はViewModelは破棄出来ないことになります。
ですが、ViewはViewModelによって管理される為、ViewModel側でViewのフェードアウト終了を感知する必要が出てきます。

Viewのフェードアウト終了時をViewModel側で感知するために、ViewModel側で感知用のプロパティを実装し、Viewとデータバインドを行い、Viewのフェードアウト時にプロパティトリガを使用して終了通知を送る事も出来ましたが、フェードアウトをする為だけにViewModel側でプロパティを実装しデータバインドを行うのは良い実装ではないと思います。
ここでViewとViewModelの間に、遷移感知用のヘルパクラスを使用したらどうなるかと思って実装してみました。

まず、目標として以下の事をあげました。

  1. Viewのアニメーション定義はViewに行いさせたい
  2. ViewとViewModelの疎結合を損ないたくない
  3. ViewModel側にViewのフェードアウト終了を意識させたくない
  4. View側でViewModelがフェードアウト終了を待機していることを意識させたくない
  5. フェードイン・フェードアウトの設定はXAMLで設定したい
以上が目標です。


また書きなぐりですので、コード例の注意点がいくつかあります。

  1. 参考サイトをベースに実装していますが、一部変更しています
  2. Viewのフェードアウトについての例なので、今回はModelは書いてありません
  3. 例外処理などはきちんと実装していません
  4. 自分で作成しているアプリケーション用の為、汎用的に使用できません
では実装結果です。


まず、View側のフェードイン・フェードアウトの設定は2パターン作りました。
リソース名指定方法と、リソース指定方法です。

- リソース名指定方法 -
<UserControl x:Class="FadeSample.View.Sample1View"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:helper="clr-namespace:FadeSample.Helper"
    helper:TransitionHelper.Attach="True"
    helper:TransitionHelper.LoadAnimationName="LoadAnimation"
    helper:TransitionHelper.CloseAnimationName="CloseAnimation"
    Opacity="0">
- リソース指定方法 -
<UserControl x:Class="FadeSample.View.Sample1View"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:helper="clr-namespace:FadeSample.Helper"
    helper:TransitionHelper.Attach="True"
    helper:TransitionHelper.LoadStoryboard="{DynamicResource LoadAnimation}"
    helper:TransitionHelper.CloseStoryboard="{DynamicResource CloseAnimation}"
    Opacity="0">

この2パターンの違いは、フェードイン・フェードアウトを定義しているリソースを名前指定で設定するか、リソース指定で設定するかの違いです。
リソース指定の場合は、少し注意する必要があります。View(UserControl)内でリソースを定義している場合はDynamicResourceでないとエラーになります。
これはUserControlのInitializeComponentメソッドが呼ばれ、XAML内が解析される時にStaticResourceの場合だとまたリソースインスタンスが生成されていない為です。

上記例でも解るとおりに、TransitionHelperという遷移感知ヘルパクラスの添付プロパティを使用してアニメーションを定義しています。


まずは今回作成したソリューション一覧です。



ではViewModelからです。
ViewModelの基底クラスとなるViewModelBaseを定義します。

- ViewModelBase.cs -
using System;
using System.ComponentModel;
 
namespace FadeSample.ViewModel
{
    public class ViewModelBase : INotifyPropertyChanged
    {
        #region RequestClosed Event
 
        /// <summary>
        /// ViewModel側終了通知用イベント
        /// </summary>
        public event EventHandler<TransitionEventArgs> RequestClose;
 
        internal void OnRequestClose(ViewModelBase transitionViewModel)
        {
            EventHandler<TransitionEventArgs> handler = this.RequestClose;
            if (handler != null)
                handler(this, new TransitionEventArgs(transitionViewModel));
        }
 
        #endregion
 
        #region ViewClose Event
 
        /// <summary>
        /// 遷移ヘルパ側終了通知用イベント
        /// </summary>
        public event EventHandler<TransitionEventArgs> ViewClose;
 
        protected void Close(ViewModelBase transitionViewModel)
        {
            EventHandler<TransitionEventArgs> handler = this.ViewClose;
            if (handler != null)
                handler(this, new TransitionEventArgs(transitionViewModel));
        }
 
        #endregion
 
        #region INotifyPropertyChanged メンバ
 
        /// <summary>
        /// プロパティ値変更通知用イベント
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;
 
        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = this.PropertyChanged;
            if (handler != null)
                handler(this, new PropertyChangedEventArgs(propertyName));
        }
 
        #endregion
    }
 
    public class TransitionEventArgs    : EventArgs
    {
        public TransitionEventArgs(ViewModelBase transitionViewModel)
        {
            this.transitionViewModel = transitionViewModel;
        }
 
        private ViewModelBase transitionViewModel;
 
        /// <summary>
        /// 遷移先ViewModel
        /// </summary>
        public ViewModelBase TransitionViewModel
        {
            get { return transitionViewModel; }
        }
    }
}

ViewModelBaseクラスではプロパティ値変更通知用のINotifyPropertyChangedインターフェイスを実装しています。
また、ViewModelインスタンスを管理しているViewModelクラスへインスタンスの破棄を通知するためのRequestCloseイベントと、遷移感知ヘルパクラスへ終了を通知する為のViewCloseイベントを実装しています。
2つのイベントでは次遷移先のViewModelを受け渡す為のTransitionEventArgsクラスも実装しています。
ViewCloseイベントが発行されると遷移感知ヘルパクラスが感知し、フェードインアニメーションを実行し終了するとRequestCloseイベントを発行するような仕組みです。

次にViewModelの切り替えをするMainWindowViewModeです。

- MainWindowViewModel.cs -
using System;
 
namespace FadeSample.ViewModel
{
    public class MainWindowViewModel : ViewModelBase
    {
        #region Constructor
 
        public MainWindowViewModel()
        {
            Workspace = new Sample1ViewModel();
        }
 
        #endregion
 
        #region Property
 
        private ViewModelBase workspace;
 
        /// <summary>
        /// Viewデータ元ViewModel
        /// </summary>
        public ViewModelBase Workspace
        {
            get { return workspace; }
            set
            {
                if (workspace == value) return;
                workspace = value;
 
                if (workspace != null)
                {
                    workspace.RequestClose += Workspace_RequestClosed;
                }
 
                OnPropertyChanged("Workspace");
            }
        }
 
        #endregion
 
        #region Event Handler
 
        /// <summary>
        /// RequestCloseイベントハンドラ
        /// </summary>
        private void Workspace_RequestClosed(object sender, TransitionEventArgs e)
        {
            Workspace.RequestClose -= Workspace_RequestClosed;
            Workspace = null;
 
            //遷移先がある場合は遷移する
            if (e.TransitionViewModel != null)
            {
                Workspace = e.TransitionViewModel;
            }
        }
 
        #endregion
    }
}

MainWindowViewModelではViewの切り替え元のデータとなるViewModelBase型のWorkspaceプロパティを実装しています。WorkspaceプロパティにViewModelBaseの派生クラスを設定することにより、DataTemplateにてViewが起動します。
またWorkspaceプロパティに設定されたViewModelより終了通知を受けるためのRequestCloseイベントもアタッチし、イベントハンドラ内でWorkspaceプロパティに設定されているViewModelを破棄し、次遷移先のViewModeインスタンスをWorkspaceプロパティに設定します。

次にMainWindowViewModelのViewにあたるMainWindowです。

- MainWindow.xaml -
<Window x:Class="FadeSample.MainWindow"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   Title="MainWindow" Height="300" Width="300"
    DataContext="{StaticResource StartupViewModel}">
    <Grid>
 
        <!-- Workspaceエリア -->
        <ContentControl
            VerticalAlignment="Center"
            HorizontalAlignment="Center"
            Content="{Binding Workspace}">
        </ContentControl>
 
    </Grid>
</Window>

MainWindowはViewを表示する元のWindowになります。今回はMainWindowViewModelのWorkspaceプロパティに設定されたViewModelをDataTemplateを使用して表示する為のContentControlを配置してあります。
また、MainWindowのDataContextプロパティに設定するMainWindowViewModelインスタンスはリソースより設定しています。

では次に、フェードイン・フェードアウトのサンプルとなるSample1ViewModelとSample2ViewModelです。

- Sample1ViewModel.cs -
using System;
using System.Windows.Input;
 
namespace FadeSample.ViewModel
{
    public class Sample1ViewModel : ViewModelBase
    {
        #region Command
 
        private ICommand command;
        public ICommand Command
        {
            get
            {
                if (command == null)
                    command = new RelayCommand(param => CloseView());
                return command;
            }
        }
 
        #endregion
 
        private void CloseView()
        {
            Close(new Sample2ViewModel());
        }
    }
}

- Sample2ViewModel.cs -
using System;
using System.Windows.Input;
 
namespace FadeSample.ViewModel
{
    public class Sample2ViewModel : ViewModelBase
    {
        #region Command
 
        private ICommand command;
        public ICommand Command
        {
            get
            {
                if (command == null)
                    command = new RelayCommand(param => CloseView());
                return command;
            }
        }
 
        #endregion
 
        private void CloseView()
        {
            Close(new Sample1ViewModel());
        }
    }
}

Sample1ViewModelもSample2ViewModelもViewのフェードイン・フェードアウトを試すためだけの簡単な実装です。
Viewが終了する際に使用するコマンドを実装しています。ここではコマンドの指定をRelayCommand型で定義していますが、RelayCommandは汎用コマンド定義クラスであり、参考サイトで詳しく説明されています。
詳細については参考サイトをご覧ください。一応RelayCommandもあげておきます。

- RelayCommand.cs -
using System;
using System.Windows.Input;
 
namespace FadeSample.ViewModel
{
    /// <summary>
    /// コマンドヘルパクラス
    /// </summary>
    public class RelayCommand : ICommand
    {
        private Action<object> execute;
        private Predicate<object> canExecute;
 
        #region Constructors
 
        public RelayCommand(Action<object> execute) : this(execute,null)
        {}
 
        public RelayCommand(Action<object> execute, Predicate<object> canExecute)
        {
            if (execute == null) throw new ArgumentNullException("execute");
            this.execute = execute;
            this.canExecute = canExecute;
        }
 
        #endregion
 
        #region ICommand メンバ
 
        public event EventHandler CanExecuteChanged;
 
        /// <summary>
        /// コマンド実行可否
        /// </summary>
        public bool CanExecute(object parameter)
        {
            return (canExecute == null ? true : canExecute(parameter));
        }
 
        /// <summary>
        /// コマンド実行
        /// </summary>
        public void Execute(object parameter)
        {
            execute(parameter);
        }
 
        #endregion
    }
}

次に遷移感知ヘルパクラスを使用したSample1ViewとSample2Viewです。

- Sample1View.xaml -
<UserControl x:Class="FadeSample.View.Sample1View"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:helper="clr-namespace:FadeSample.ViewModel"
    helper:TransitionHelper.Attach="True"
    helper:TransitionHelper.LoadStoryboard="{DynamicResource LoadAnimation}"
    helper:TransitionHelper.CloseStoryboard="{DynamicResource CloseAnimation}"
    Opacity="0">
 
    <UserControl.Resources>
        <!-- Load時アニメーション -->
        <Storyboard x:Key="LoadAnimation">
            <DoubleAnimation Storyboard.TargetProperty="Opacity" From="0" To="1" Duration="00:00:0.5" />
        </Storyboard>
        <!-- Close時アニメーション -->
        <Storyboard x:Key="CloseAnimation">
            <DoubleAnimation Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="00:00:0.5" />
        </Storyboard>
    </UserControl.Resources>
 
    <Border Background="Red" Width="150" Height="150">
        <Button Width="100" Height="30" Command="{Binding Path=Command}">Sample2Viewへ</Button>
    </Border>
 
</UserControl>

- Sample2View.xaml -

<UserControl x:Class="FadeSample.View.Sample2View"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:helper="clr-namespace:FadeSample.ViewModel"
    helper:TransitionHelper.Attach="True"
    helper:TransitionHelper.LoadAnimationName="LoadAnimation"
    helper:TransitionHelper.CloseAnimationName="CloseAnimation"
    Opacity="0" RenderTransformOrigin="0.5,1">
 
    <UserControl.Resources>
        <!-- Load時アニメーション -->
        <Storyboard x:Key="LoadAnimation">
            <DoubleAnimation Storyboard.TargetProperty="Opacity" From="0" To="1" Duration="00:00:0.5" />
            <DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)"
                    From="0" To="1" Duration="00:00:0.5" />
        </Storyboard>
        <!-- Close時アニメーション -->
        <Storyboard x:Key="CloseAnimation">
            <DoubleAnimation Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="00:00:0.5" />
            <DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)"
                    From="1" To="0" Duration="00:00:0.5" />
        </Storyboard>
    </UserControl.Resources>
 
    <UserControl.RenderTransform>
        <ScaleTransform ScaleX="1"    ScaleY="0" />
    </UserControl.RenderTransform>
 
    <Border Background="Blue" Width="150" Height="150">
        <Button Width="100" Height="30" Command="{Binding Command}">Sample1Viewへ</Button>
    </Border>
 
</UserControl>

Sample1Viewではリソース指定で遷移感知ヘルパクラスを使用しています。フェードイン・フェードアウトのアニメーションを自身のリソース内にて定義しているのでDynamicResourceとなっています。
Sample2Viewではリソース名指定で遷移感知ヘルパクラスを使用しています。
今回Sample1ViewではOpacityを使用した徐々にViewが描画されるフェードイン・フェードアウトを。Sample2ViewではRenderTransformを使用した下から出てくるようなフェードイン・フェードアウトを指定してます。

遷移管理ヘルパクラスの使用する場合の注意点としては、あくまでもViewに対しアニメーションをかけるのでViewに対して初期設定を行うことです。
子要素に対してフェードイン・フェードアウトをかけるのでは無い事に注意して下さい。

今回ViewとViewModelのDataTemplateはApp.xamlに設定してます。ではその設定です。

- App.xaml -
<Application x:Class="FadeSample.App"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:viewModel="clr-namespace:FadeSample.ViewModel"   
    xmlns:view="clr-namespace:FadeSample.View"
   StartupUri="MainWindow.xaml">
    <Application.Resources>
 
        <!-- Startup ViewModel -->
        <viewModel:MainWindowViewModel x:Key="StartupViewModel" />
 
        <!-- ViewMode → View DataTemplate-->
        <DataTemplate DataType="{x:Type viewModel:Sample1ViewModel}">
            <view:Sample1View />
        </DataTemplate>
        <DataTemplate DataType="{x:Type viewModel:Sample2ViewModel}">
            <view:Sample2View />
        </DataTemplate>
 
    </Application.Resources>
</Application>

App.xamlにてViewとViewModelの関連をDataTemplateにて定義しています。
MainWindowのContentControlのContentプロパティにバインドされているMainWindowViewModelのWorkspaceプロパティにViewModelが設定されるとViewが起動するわけです。

では最後に遷移感知ヘルパクラスです。

- TransitionHelper.cs -
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Media.Animation;
using FadeSample.ViewModel;
 
namespace FadeSample.Helper
{
    /// <summary>
    /// View遷移ヘルパクラス
    /// </summary>
    public static class TransitionHelper
    {
        #region Fields
 
        /// <summary>
        /// View - ViewModel保持用
        /// </summary>
        private static Dictionary<ViewModelBase, FrameworkElement> targets =
            new Dictionary<ViewModelBase, FrameworkElement>();
 
        /// <summary>
        /// 遷移ヘルパアタッチ用添付プロパティ
        /// </summary>
        public static readonly DependencyProperty AttachProperty =   
            DependencyProperty.RegisterAttached("Attach", typeof(bool), typeof(TransitionHelper),
            new PropertyMetadata(false,new PropertyChangedCallback(AttachPropertyChangedCallback)));
 
        /// <summary>
        /// 起動時アニメーションリソース名
        /// </summary>
        public static readonly DependencyProperty LoadAnimationNameProperty =
            DependencyProperty.RegisterAttached("LoadAnimationName", typeof(string), typeof(TransitionHelper));
 
        /// <summary>
        /// 終了時アニメーションリソース名
        /// </summary>
        public static readonly DependencyProperty CloseAnimationNameProperty =
            DependencyProperty.RegisterAttached("CloseAnimationName", typeof(string), typeof(TransitionHelper));
 
        /// <summary>
        /// 起動時アニメーション
        /// </summary>
        public static readonly DependencyProperty LoadStoryboardProperty =
            DependencyProperty.RegisterAttached("LoadStoryboard", typeof(Storyboard), typeof(TransitionHelper));
 
        /// <summary>
        /// 終了時アニメーション
        /// </summary>
        public static readonly DependencyProperty CloseStoryboardProperty =
            DependencyProperty.RegisterAttached("CloseStoryboard", typeof(Storyboard), typeof(TransitionHelper));
 
        #endregion
 
        #region Metadata Callback Method
 
        /// <summary>
        /// AttachProperty変更時コールバック
        /// </summary>
        private static void AttachPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            FrameworkElement target = (FrameworkElement)d;
 
            //デザインモード時は遷移ヘルパ機能を使用しない
            if (DesignerProperties.GetIsInDesignMode(target)) return;
 
            target.DataContextChanged += target_DataContextChanged;
            target.Loaded += target_Loaded;
        }
 
        #endregion
 
        #region EventHandlers
 
        /// <summary>
        /// ViewModel設定時
        /// </summary>
        private static void target_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            FrameworkElement target = (FrameworkElement)sender;
            target.DataContextChanged -= target_DataContextChanged;
 
            ViewModelBase viewModel = target.DataContext as ViewModelBase;
            if (viewModel == null) throw new ApplicationException("View遷移にはViewModelが必要です。");
            viewModel.ViewClose += viewModel_ViewClose;
 
            targets.Add(viewModel, target);
        }
 
        /// <summary>
        /// View開始時
        /// </summary>
        private static void target_Loaded(object sender, RoutedEventArgs e)
        {
            FrameworkElement target = (FrameworkElement)sender;
            target.Loaded -= target_Loaded;
 
            Storyboard loadStoryboard = GetLoadStoryboard(target);
            if (loadStoryboard == null)
            {
                loadStoryboard = GetLoadStoryboardByName(target);
            }
 
            if (loadStoryboard != null)
                target.BeginStoryboard(loadStoryboard);
        }
 
        /// <summary>
        /// ViewModel終了時
        /// </summary>
        private static void viewModel_ViewClose(object sender, TransitionEventArgs e)
        {
            ViewModelBase viewModel = (ViewModelBase)sender;
            viewModel.ViewClose -= viewModel_ViewClose;
            FrameworkElement target = targets[viewModel];
 
            Storyboard closeStoryboard = GetCloseStoryboard(target);
            if (closeStoryboard == null)
            {
                closeStoryboard = GetCloseStoryboardByName(target);
            }
 
            if (closeStoryboard != null)
            {
                closeStoryboard.Completed += (obj, args) =>
                {
                    viewModel.OnRequestClose(e.TransitionViewModel);
                };
 
                target.BeginStoryboard(closeStoryboard);
            }
            else
            {
                viewModel.OnRequestClose(e.TransitionViewModel);
            }
 
            targets.Remove(viewModel);
        }
 
        #endregion
 
        #region Attached Method
 
        public static bool GetAttach(DependencyObject obj)
        {
            return (bool)obj.GetValue(AttachProperty);
        }
        public static void SetAttach(DependencyObject obj, bool value)
        {
            obj.SetValue(AttachProperty, value);
        }
 
        public static string GetLoadAnimationName(DependencyObject obj)
        {
            return (string)obj.GetValue(LoadAnimationNameProperty);
        }
        public static void SetLoadAnimationName(DependencyObject obj, string value)
        {
            obj.SetValue(LoadAnimationNameProperty, value);
        }
 
        public static string GetCloseAnimationName(DependencyObject obj)
        {
            return (string)obj.GetValue(CloseAnimationNameProperty);
        }
        public static void SetCloseAnimationName(DependencyObject obj, string value)
        {
            obj.SetValue(CloseAnimationNameProperty, value);
        }
 
        public static Storyboard GetLoadStoryboard(DependencyObject obj)
        {
            return (Storyboard)obj.GetValue(LoadStoryboardProperty);
        }
        public static void SetLoadStoryboard(DependencyObject obj, Storyboard value)
        {
            obj.SetValue(LoadStoryboardProperty, value);
        }
 
        public static Storyboard GetCloseStoryboard(DependencyObject obj)
        {
            return (Storyboard)obj.GetValue(CloseStoryboardProperty);
        }
        public static void SetCloseStoryboard(DependencyObject obj, Storyboard value)
        {
            obj.SetValue(CloseStoryboardProperty, value);
        }
 
        #endregion
 
        #region Private Methods
 
        /// <summary>
        /// Load時アニメーション取得
        /// </summary>
        private static Storyboard GetLoadStoryboardByName(FrameworkElement target)
        {
            Storyboard result = (Storyboard)target.Resources[GetLoadAnimationName(target)];
            return result;
        }
 
        /// <summary>
        /// Close時アニメーション取得
        /// </summary>
        private static Storyboard GetCloseStoryboardByName(FrameworkElement target)
        {
            Storyboard result = (Storyboard)target.Resources[GetCloseAnimationName(target)];
            return result;
        }
 
        #endregion
    }
}

ちょっと長いコードですがひとつづつ説明します。
まずView側のXAMLにてアニメーションを設定する添付プロパティを実装します。

  • AttachProperty
  • LoadAnimationNameProperty
  • CloseAnimationNameProperty
  • LoadStoryboardProperty
  • CloseStoryboardProperty
AttachPropertyは、View側よりフェードイン・フェードアウトが設定される事を感知し、View(UserControl)のイベントをアタッチします。アタッチポイントは、添付プロパティのメタデータのPropertyChangedCallback内で行います。
PropertyChangedCallback内では以下のイベントアタッチを行います。

  • ViewのDataContextChangedイベント
  • ViiewのLoadedイベント
DataContextChangedイベントは、DataTemplateによってViewのDataContextプロパティに設定されたViewModelインスタンスを取得する為です。
Loadedイベントは、Viewのフェードインに使用します。
あとの添付プロパティはXAMLより設定される各アニメーション指定用の添付プロパティです。

まずはじめに、MainWindowViewModelのWorkspaceプロパティにViewModelが設定されると、ViewのDataContextChangeイベントが呼ばれます。
そのViewのDataContextChangedイベントハンドラ内で、ViewのDataContextに設定されているViewModelを取得し、ViewModelのViewCloseイベントをアタッチします。
ViewCloseイベントハンドラではViewのフェードアウトを実行し、ViewModelのOnRequestCloseメソッドをコールし、ViewModelを管理しているMainWindowViewModelへ終了通知を伝えます。
また、ViewCloseイベントハンドラないにてViewのアニメーションを実行する為にViewインスタンスが必要になるので、ViewModelをキーとしたコレクションに一時保持しておきます。

次にViewの起動時でLoadedイベントが呼ばれます。
ViewのLoadedイベントハンドラ内にてフェードイン用のアニメーションを実行し、Viewが無事フェードインを行いながら起動します。

Viewでボタンがクリックされると、ViewModelに実装しているコマンドが呼ばれ、ViewCloseイベントが呼ばれます。
その呼ばれたViewCloseイベントのイベントハンドラないにて、一時保持していたコレクションよりViewインスタンスを取得し、フェードアウトを実行します。
ここでフェードアウトで使用しているStoryboardのCompletedイベントにてViewModelのOnRequestCloseメソッドを実行することにより、ViewModelがRequestCloseイベントを発行し、そのイベントをアタッチしているMainWindowViewModelのイベントハンドラが呼ばれることによってViewが終了するようになります。
これで一連の流れは終了です。

遷移感知ヘルパクラスはViewとViewModelにドップリ依存していますが、この中間ヘルパクラスを使用することによってViewとViewModelの不必要な依存を無くすことが出来ます。
また、ViewModel側もViewCloseイベント発行用のCloseメソッドをコールすればViewを遷移させることが出来るため、よけいな実装を行う必要がなくなります。

今回の実装はいくつかまだ足りない部分がありますが、とりあえずの実装の形にはなったかなと思います。
添付プロパティを使用したヘルパクラスの実装はいくらでも出来るので、アプリケーションの構築に合わせたものを実装することも可能です。
アニメーションは画像では伝えることが出来ないので添付画像は載せていません。
興味のある方は是非実装して試してみてください。


今回の感想として、ヘルパクラスを実装してみた感じ、MVVMパターンではViewとViewModelの橋渡しをするヘルパクラスが多々必要になってくるのかなと思いました。
フェードイン・フェードアウトのような少しのインタラクションを実現するためにも面倒な実装を強いられますが、それが出来たときのアプリケーションの表現の幅は大きいのではないでしょうか。

今回はMVVMパターンにおけるViewのフェードイン・フェードアウトを実現するためのほんの1例でした。

フィードバック

# re: [WPF] MVVMを使用した場合のViewのフェードイン・フェードアウト用ヘルパクラスの実装

2009/04/22 17:37 by trapemiya
いやぁ、すばらしいです。早速、参考にさせていただきました。
私もこういったヘルパクラスのようなクラスを想像していました。ViewとViewModelはこういったヘルパクラス(プロキシクラス)を経由して初めて疎結合されていると言えるのかもしれません。DataContextだけに設定しているから疎結合だと言えなくもないかもしれませんが、私の中では単なる"疎"の関係で結合しているという状態ではないという認識です。

# re: [WPF] MVVMを使用した場合のViewのフェードイン・フェードアウト用ヘルパクラスの実装

2009/04/23 9:30 by kazuto
>trapemiyaさん
コメント有難う御座います。
何とか実現したかった事の基盤的な物はなんとか出来ました。
前回のtrapemiyaさんのコメントがヒントとなりました。有難うございました。
DataContextによってViewはでViewModelに依存していると言えると思いますが、そこはDataTemplateがうまく隠してくれているため、開発者視点では疎結合感を感じれているのかと思ってます。
依存性と疎結合は色々な捉え方があると思いますが、開発者視点での「疎」を提供するにはこのようなバックエンドで頑張ってくれるクラスが必要だと改めて感じました。
WPFのプロパティシステムはプロキシクラスなどを作成するのにとても向いていると感じています。
コメントの入力
タイトル
名前
Url
コメント