[WPF][MVVM][Prism] InteractionRequest

はじめに

MVVMのViewModelの処理で、処理の途中にユーザへの問い合わせをして以降の処理を分岐させたい(例:「警告:重複したデータがありますが上書きしますか?」→「はい」を選択したら継続し、「いいえ」を選択したら終了する)ような場合、ViewModelから確認ダイアログを表示すると、Viewへの依存を持つことになり、また自動テストもできなくなってしまう。Prismではそういったユーザ問い合わせを抽象化した仕組みがInteractionRequest名前空間にあるクラス・インタフェース群に存在するので使ってみる

準備

プロジェクトの参照に、次の2つのアセンブリを追加する

  • Microsoft.Practices.Prism.Interactivity.dll
    • PrismをインストールしたディレクトリのSource\bin\Desktopにあるはず
    • 他のPrismアセンブリと同様に、Library.Desktopにコピーしてプロジェクトから参照する
  • System.Windows.Interactivity.dll
    • Expression Blend 4をインストールしているPCであればおそらく.NETのコンポーネントとして一覧に表示されるはず
    • 自分はExpression Blendをインストールしていない(!)ので、PrismをインストールしたディレクトリのSource\Lib\Desktopにあったものを使ってみる

実装

まず、ViewModelにInteractionRequestをプロパティとして保持する
FileListViewModel.cs

public class FileListViewModel : NotificationObject, IConfirmNavigationRequest
{
    private readonly InteractionRequest<Confirmation> confirmNavigationRequest =
        new InteractionRequest<Confirmation>();

    public IInteractionRequest ConfirmNavigationRequest
    {
        get
        {
            return this.confirmNavigationRequest;
        }
    }
}

xamlにSystem.Windows.Interactivityを使えるように、名前空間を追加する

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

Interaction.Triggersを定義する

<UserControl x:Class="WindowsExplorerish.Views.FileListView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:prism="http://www.codeplex.com/prism"
             xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <i:Interaction.Triggers>
        <prism:InteractionRequestTrigger
            SourceObject="{Binding ConfirmNavigationInteractionRequest}">
            <!-- TriggerActionをここに定義 -->
        </prism:InteractionRequestTrigger>
    </i:Interaction.Triggers>

    <!-- (略) -->

</UserControl>

<prism:InteractionRequestTrigger>は、ConfirmNavigationInteractionRequestプロパティ(InteractionRequest型)のRaisedイベント発火によって発生するトリガを定義する。
このトリガが発生したときのTriggerAction(今回は確認ダイアログを出すAction)を定義するのだが、後述する。

戻ってViewModelで、Interactionを発生させるコードを書く。
FileListViewModel.cs

        public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
        {
            if (this.IsNavigationLocked)
            {
                this.confirmNavigationRequest.Raise(
                            new Confirmation() { Title = "Confirm", Content = "Navigationがロックされていますが、強制的に開きますか?" },
                            c => continuationCallback(c.Confirmed));
            }
            else
            {
                continuationCallback(true);
            }
        }

InteractionRequest.Raise()に、確認内容を抽象化したオブジェクト(Confirm)を渡す。2つ目のパラメータは確認結果を渡してcontinuationCallbackをコールするコールバックを渡している(ややこしい)

continuationCallbackをそのままRaise()に渡したいところだが、メソッドシグネチャが違うためできない。Callbackの形式をとっているのは非同期的な処理を扱うためだろうか。やってみないとあまり納得感がないが。

この状態で実行すると、Navigationが常にキャンセルされる。

                this.confirmNavigationRequest.Raise(
                            new Confirmation() { Title = "Confirm", Content = "Navigationがロックされていますが、強制的に開きますか?" },
                            c => continuationCallback(c.Confirmed));

デバッガで確認すると、Confirmation.Confirmedプロパティが常にfalseを応答しているようだ。

では確認ダイアログを表示するカスタムTriggerActionを実装する。

SilverlightだとPopupChildWindowActionなどというActionが存在するのだが、WPFにはない。確認ダイアログの見た目や選択肢は様々なのでデフォルトのアクションを用意しても仕方ないという判断なのかもしれないが。

新規にShowMessageBoxActionクラスをViewsフォルダに追加する

namespace WindowsExplorerish.Views
{
    public class ShowMessageBoxAction : TriggerAction<FrameworkElement>
    {
        protected override void Invoke(object parameter)
        {
            InteractionRequestedEventArgs e = parameter as InteractionRequestedEventArgs;
            if (e != null)
            {
                var title = e.Context.Title;
                var message = e.Context.Content.ToString();

                if (e.Context is Confirmation)
                {
                    var result = MessageBox.Show(message, title, MessageBoxButton.YesNo, MessageBoxImage.Question);
                    ((Confirmation)e.Context).Confirmed = result == MessageBoxResult.Yes;
                }
                else
                {
                    MessageBox.Show(message, title, MessageBoxButton.OK, MessageBoxImage.Information);
                }

                e.Callback();
            }
        }
    }
}

TriggerAction.Invoke()にはInteractionRequestTrigger.SourceObjectに定義したオブジェクト(ConfirmNavigationInteractionRequestプロパティ)がパラメータで渡される。この実装では Confirmationか Notificationかでメッセージダイアログの選択肢とアイコンを変更するようにした。
そしてユーザとのInteractionを終えたことを知らせるためにe.Callback()をコールする

次にこのActionをxamlに定義する。

<UserControl x:Class="WindowsExplorerish.Views.FileListView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:prism="http://www.codeplex.com/prism"
             xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
             xmlns:localview="clr-namespace:WindowsExplorerish.Views"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <i:Interaction.Triggers>
        <prism:InteractionRequestTrigger
            SourceObject="{Binding ConfirmNavigationInteractionRequest}">
            <localview:ShowMessageBoxAction />
        </prism:InteractionRequestTrigger>
    </i:Interaction.Triggers>

    <!-- (略) -->

</UserControl>

これで実行すると Navigationのたびに確認ダイアログが表示され、「はい」を押すとNavigationが実行され、「いいえ」を押すと元のままである。
Confirm

まとめ

  • ユーザへの確認や通知ダイアログをViewModelトリガで行う場合は Interactivityを使う
  • ViewModelにIInteractionRequestをプロパティを実装し、ViewのInteractionTrigger.SourceObjectとdatabindする
  • MessageBoxを表示するTriggerActionはWPFのデフォルトにはない(Silverlightにはある)ので実装する
    • Interactionが完了したことを知らせるために、callbackを確認結果を渡して呼び出す

参考:
Developer’s Guide to Microsoft Prism Chapter 8: Navigation Confirming or Cancelling Navigation

これまでの目次

広告

[WPF][Prism] Navigationのキャンセル

はじめに

Prismでは、Navigationが発生したときに、現在アクティブなViewの状態に応じて、それをキャンセル(Navigationがなかったことに)することができる。今回はそれについて説明する。

実現方法

まずはIConfirmNavigationRequestインタフェースをViewまたはViewModelに実装する。(どちらでも良い)
この例では MVVM的にViewModelが状態やキャンセルするかしないかを判断するようにしてみる。
NavigationのRequestが発生した時、Navigation対象のRegionでActiveなViewのConfirmNavigationRequest()が呼び出されるようだ。

FileListViewModel.cs

public class FileListViewModel : NotificationObject, IConfirmNavigationRequest

IConfirmNavigationRequestインタフェースの4つのメソッドを実装する。(うち3つは以前の記事「Navigation」で説明したINavigationAwareのメソッドなので説明を省略する)

public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
{
    bool canNavigate = this.IsNavigationLocked == false;
    continuationCallback(canNavigate);
}

continuationCallbackのパラメータにNavigationが継続可能かどうかをboolで渡せばよい。
なぜコールバックなのか。(あるいは、なぜ boolConfirmNavigationRequest()メソッドが返却しないのか)については、よくわかっていない。

まとめ

  • NavigationのキャンセルができるのはIConfirmNavigationRequestインタフェースを実装したViewまたはViewModel
  • Navigation対象のRegionで現在ActiveなView(またはそのViewModel)のConfirmNavigationRequest()が呼ばれる
  • continuationCallbackのパラメータにNavigationが継続可能かどうかをboolで渡す

これまでの目次

[WPF][MVVM][Prism] Navigation

はじめに

様々なビューが1つのWindowに統合されているCompositeな複合UIでは、Window全体ではなく区画ごとに画面が切り替わるようなUIが望ましい。Prismでは、Regionごとに画面を遷移させることができる機能がある。
画面遷移のAPIはいくつかあるが、ここでは遷移先の画面をURIで指定する方法を使う。

Navigation の実行


var regionManager = container.Resolve<IRegionManager>();
regionManager.RequestNavigate("RegionName", "NewView");

URIの文字列"NewView"を指定すると、Unityにその名前で登録されているViewを取得してNavigationを実行する仕様なので、事前に"NewView"の登録が必要

container.RegisterType<object, NewView>("NewView");

URIなのでパラメータを指定することもできる

regionManager.RequestNavigate("RegionName", "NewView?id=1&name=new");

またRequestNavigatestringだけでなくUriを指定できるメソッドもある。

Navigation 先の処理

ViewまたはViewModelパラメータを受け取るには、INavigationAwareインタフェースを実装する。Viewでもよいし、ViewModelでも良い。(PrismではNavigationのときに、Viewに実装されていなければ、ViewModelに実装されているかをチェックしている)。Navigationが発生すると、遷移先のViewでOnNavigatedTo()が呼び出される。メソッドのパラメータでNavigationContextを受け取り、ParametersでURIのパラメータ部分を取得してViewに反映させる。
他の2つメソッドはそれぞれ、
OnNavigatedFrom() : Navigationが発生し、新しいViewが現れる前のアクティブなViewで呼ばれる。データを保存したりする。
IsNavigationTarget() : 既に開いているViewに遷移したいときに、Trueを応答すると、そのViewがアクティブ化される
もうひとつ。INavigationAwareを拡張した、IConfirmNavigationRequestインタフェースというものがある。

public interface IConfirmNavigationRequest : INavigationAware

これのConfirmNavigationRequestメソッドを実装すると、Navigationをキャンセルできる。(例:変更は保存されませんがよろしいでしょうか?→[いいえ]を押されたとき)
Navigationのキャンセルについては別の機会に。

前回までに実装したWindowsエクスプローラ風のビューに実装

Navigationを前回までに実装したWindowsエクスプローラ風のビューに実装してみる。
フォルダツリーで選択されたフォルダをファイル一覧に表示する。(ファイル一覧の表示区域(“MainRegion”)だけが切り替わる)

まずは、URIで指定できるようにUnityに登録する
WindowsExplorerishModule.cs

public class WindowsExplorerishModule : IModule
{
    private readonly IUnityContainer container;
    private readonly IRegionManager regionManager;

    public WindowsExplorerishModule(IUnityContainer container, IRegionManager regionManager)
    {
        this.container = container;
        this.regionManager = regionManager;
    }

    public void Initialize()
    {
        this.container.RegisterType<object, FolderTreeView>("FolderTreeView");
        this.container.RegisterType<object, FileListView>("FileListView");

        this.regionManager.RequestNavigate("LeftRegion", "FolderTreeView");
        this.regionManager.RequestNavigate("MainRegion", "FileListView");
    }
}

初期表示として画面遷移APIを呼び出す

    public void Initialize()
    {
        this.container.RegisterType<object, FolderTreeView>("FolderTreeView");
        this.container.RegisterType<object, FileListView>("FileListView");

        this.regionManager.RequestNavigate("LeftRegion", "FolderTreeView");
        this.regionManager.RequestNavigate("MainRegion", "FileListView");
    }

前回、フォルダツリーの選択状態をViewModelのプロパティにdatabindingしたので、そのプロパティ変更処理で画面遷移を実行する

FolderTreeViewModel.cs

public class TreeNode : NotificationObject
{
    private readonly DirectoryInfo dirInfo
    private bool isSelected = false;

    public bool IsSelected
    {
        get
        {
            return this.isSelected;
        }

        set
        {
            if (this.isSelected != value)
            {
                this.isSelected = value;
                if (this.isSelected)
                {
                    var regionManager = ServiceLocator.Current.GetInstance<IRegionManager>();
                    regionManager.RequestNavigate("MainRegion", "FileListView?path=" + this.dirInfo.FullName);
                }

                this.RaisePropertyChanged(() => this.IsSelected);
            }
        }
    }

    // 他のコードは変更なしのため省略
}

URIはこのような形でNavigation実行

FileListView?path=c:\work

他の画面遷移の方法として、AddToRegion()RegisterViewWithRegion()といったAPIもあるが、ViewModelから View を参照してしまうため、MVVMの原則に合致しない問題がある。

var regionManager = container.Resolve<IRegionManager>();

regionManager.AddToRegion("RegionName", new NewView());
regionManager.RegisterViewWithRegion("RegionName", typeof(NewView));

FileListViewModel はのプロパティはread-onlyで、常に Directory.GetCurrentDirectory()の内容を表示していた。今回は、指定されたパスのファイル一覧を表示するため、プロパティを変更可能にする。Pathプロパティが変更されるとき、変更通知を発生させてViewに変更を伝播させる。
FileListViewModel.cs

public class FileListViewModel : NotificationObject, INavigationAware
{
    private string path = null;

    public string Path
    {
        get
        {
            return this.path;
        }

        set
        {
            if (this.path != value)
            {
                this.path = value;

                // raise property changed notifications
                this.RaisePropertyChanged(() => this.Files);
            }
        }
    }

    public IEnumerable<FileViewModel> Files
    {
        get
        {
            try
            {
                return Directory
                        .EnumerateFiles(this.Path)
                        .Select(s => new FileViewModel(s));
            }
            catch (Exception)
            {
                return null;
            }
        }
    }
}

INavigationAwareインタフェースを実装する。
FileListViewModel.cs

public class FileListViewModel : NotificationObject, INavigationAware

3つのメソッドを実装する。Navigationが発生すると、ViewとViewModelが生成され、その後にOnNavigatedTo()が呼ばれるため、URIパラメータからパスを取得し、Pathプロパティに設定する。
FileListViewModel.cs

public bool IsNavigationTarget(NavigationContext navigationContext)
{
    var targetPath = navigationContext.Parameters["path"];
    return this.Path == targetPath;
}

public void OnNavigatedFrom(NavigationContext navigationContext)
{
    // do nothing
}

public void OnNavigatedTo(NavigationContext navigationContext)
{
    if (this.Path == null)
    {
        var newPath = navigationContext.Parameters["path"];

        this.Path = newPath ?? Directory.GetCurrentDirectory();
    }
}

この状態でアプリケーションを実行して、ツリービューのフォルダをクリックすると、ファイルリストが切り替わる。
これだと良くわからないので、次回はファイルリストの表示区域(”MainRegion”)をタブ化してみる。

まとめ

  • Navigationには Region名と遷移先画面を特定するURIを指定して実行する
  • 画面の URIは Unityへの登録名で決まる
  • URIにパラメータを指定することで、遷移時に画面へのパラメータを渡すことが出来る
  • Navigation先の画面はINavigationAwareインタフェースを実装することでパラメータを受け取り、画面に反映させる

これまでの目次

[WPF][Prism] PrismとTabControl

はじめに

これまでの記事では NavigationによってRegionに表示しているViewを切り替える方法について説明した。PrismのNavigationには、RegionにViewが追加されていくNavigationもある。今回は TabコントロールへViewを追加してみる。

前回のビューをタブ化する

タブ化といっても、これだけ。

 <ContentControl Name="MainRegion" prism:RegionManager.RegionName="MainRegion" />

 ↓

 <TabControl Name="MainRegion" prism:RegionManager.RegionName="MainRegion" />

Navigation対象のRegionがContentControlのサブクラスであるか、ItemsControlのサブクラスであるかによって、Viewが切り替わるのか追加されるのかという振る舞いが変わる。Navigationを指示する側はそれを特に意識しない。
なお、そういったRegionの振る舞いはIRegionAdapterを実装したクラスでカスタマイズができる。これについては別の機会に記事にしたい。

エクスプローラ風アプリケーションのShellに反映させるとこのようになる。
Shell.xaml

<Window x:Class="WpfApplication.Shell"
        xmlns:prism="http://www.codeplex.com/prism"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="150*" />
            <ColumnDefinition Width="300*" />
        </Grid.ColumnDefinitions>
        <ContentControl Name="LeftRegion" prism:RegionManager.RegionName="LeftRegion" Grid.Column="0" />
        <TabControl Name="MainRegion" prism:RegionManager.RegionName="MainRegion" Grid.Column="1" />
    </Grid>
</Window>

これでアプリケーションを実行してみると、こうなるはず。

Tabのツマミがちっさ!
これはTabItemのタイトル(Headerプロパティ)に何も設定されていないためである。

タブのタイトル(Header)をMVVM的に設定する

タブのタイトルを設定するために、「Prism and WPF. Custom Tab region adapter. Part 01. –Raffaeu Bermuda Blog」という記事に方法が載っていた。
ここでは記事よりもう少し簡単な方法で実現する。
ViewModelにContentTitleプロパティを実装し、それとTabItem.Headerとを databindする。

Shell.xamlにこのようなStyleを定義する。

    <Window.Resources>
        <Style TargetType="{x:Type TabItem}" x:Key="TabHeaderStyle">
            <Setter Property="Header" 
                    Value="{Binding RelativeSource={RelativeSource Self},  Path=Content.DataContext.ContentTitle}" />
        </Style>
    </Window.Resources>

当然、TabControlにそのStyleを参照させる

 <TabControl Name="MainRegion" prism:RegionManager.RegionName="MainRegion"
    ItemContainerStyle="{StaticResource TabHeaderStyle} />

ViewModelにContentTitleプロパティを実装する

    public class FileListViewModel : NotificationObject, INavigationAware
    {

        // 略

        public string ContentTitle
        {
            get
            {
                if (this.Path == null)
                {
                    return null;
                } 
                
                return new DirectoryInfo(this.Path).Name;
            }
        }
    }

これだけ。

早速実行すると、このようにタブにContentTitleの文字列が設定される

まとめ

  • TabControlコントロールにRegionを設定すると、そのRegionへのNavigationごとにTabItemが増える
  • TabItem.Headerとdatabindして、タイトルを設定する

これまでの目次

[WPF][Prism] Windowsエクスプローラ的なView

はじめに

前回はWPFとPrismでHello Worldを表示した。
今回はWindowsエクスプローラのフォルダーツリーとファイル一覧をTreeViewDataGridでそれぞれ実装する。
最低限の表示をするところまでなので、Prismは今回あまり触れないかも。

クラスの追加

前回追加したWindowsExplorerishプロジェクトに Viewsディレクトリの他に ViewModelsディレクトリも作成する。
Viewsにはユーザーコントロール(WPF)で、FolderTreeView.xamlとFileListView.xamlを追加。(前回つくったUserControl1.xamlはもう使わないので削除する)
ViewModelsにはそれぞれのViewModelを追加

ファイル構成はこのようになる

WindowsExplorerish
├─ViewModels
│      FileListViewModel.cs
│      FolderTreeViewModel.cs
│
└─Views
        FileListView.xaml
        FileListView.xaml.cs
        FolderTreeView.xaml
        FolderTreeView.xaml.cs

フォルダーツリー

FolderTreeViewにTreeViewを定義する。
方法 : 深度がわからないデータに TreeView をバインドする (MSDN)にあるように、HierarchicalDataTemplateを使う。

FolderTreeView.xaml

<UserControl x:Class="WindowsExplorerish.Views.FolderTreeView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <TreeView x:Name="TreeView" ItemsSource="{Binding TreeContent}" >
        <TreeView.ItemTemplate>
            <HierarchicalDataTemplate ItemsSource="{Binding Children}">
                <StackPanel Orientation="Horizontal" Height="20">
                    <TextBlock Text="{Binding Label}"/>
                </StackPanel>
            </HierarchicalDataTemplate>
        </TreeView.ItemTemplate>
    </TreeView>
</UserControl>

次にViewModelを実装する。まずxamlで定義したTreeView.ItemsSourceにバインドされる、TreeContentプロパティを実装する。今回はWindowsのドライブ一覧をトップレベルのフォルダとして表示する。
TreeViewItemとバインドするViewModelとしてTreeNodeクラスを実装し(後述)、TreeContentはそれの列挙子(IEnumerable)を返す。

FolderTreeViewModel.cs

    public class FolderTreeViewModel
    {
        public IEnumerable<TreeNode> TreeContent
        {
            get
            {
                return DriveInfo.GetDrives().Select(d => new TreeNode(d.Name));
            }
        }

        #region inner classes

        public class TreeNode  ...
        // 後述 ...

        #endregion
    }

次にTreeNodeクラス。HierarchicalDataTemplateで定義したItemsSourceにバインドされる、Childrenプロパティと、Labelプロパティを実装する。

        #region inner classes

        public class TreeNode
        {
            private readonly DirectoryInfo dirInfo;

            public TreeNode(string dirPath)
            {
                this.dirInfo = new DirectoryInfo(dirPath);
            }

            public string Label
            {
                get
                {
                    return this.dirInfo.Name;
                }
            }

            public IEnumerable<TreeNode> Children
            {
                get
                {
                    try
                    {
                        return Directory
                            .EnumerateDirectories(this.dirInfo.FullName)
                            .Select(s => new TreeNode(s));
                    }
                    catch (Exception)
                    {
                        // folder permission is handled by catching exception
                        return null;
                    }
                }
            }
        }

        #endregion

ファイルリスト

FileListViewにDataGridを定義する。
今回はグリッドのカラムは、名前、更新日時、種類、サイズと定義した。

FileListView.xaml

<UserControl x:Class="WindowsExplorerish.Views.FileListView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <StackPanel Orientation="Vertical">
        <TextBlock Text="{Binding Path}" />
        <DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Files}" IsReadOnly="True">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding Name}" Header="Name"/>
                <DataGridTextColumn Binding="{Binding LastUpdateDate}" Header="Update"/>
                <DataGridTextColumn Binding="{Binding Type}" Header="Type"></DataGridTextColumn>
                <DataGridTextColumn Binding="{Binding Size}" Header="Size"></DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>
    </StackPanel>
</UserControl>

同じようにViewModelを実装する。DataGrid.ItemsSourceにバインドされる、Filesプロパティを実装する。Windowsエクスプローラにはフォルダとファイルが表示されるが、今回はファイルだけが表示されるところまでを実装する。DataGridRowとバインドするViewModelとしてFileViewModelクラスを実装し(後述)、Filesはそれの列挙子(IEnumerable)を返す。

FileListViewModel.cs

    public class FileListViewModel : NotificationObject
    {
        public string Path
        {
            get
            {
                return Directory.GetCurrentDirectory();
            }
        }

        public IEnumerable<FileViewModel> Files
        {
            get
            {
                try
                {
                    return Directory
                            .EnumerateFiles(this.Path)
                            .Select(s => new FileViewModel(s));
                }
                catch (Exception)
                {
                    return null;
                }
            }
        }

        #region inner classes

        public class FileViewModel ...
        // 後述

        #endregion
}

FileViewModel クラス。DataGridColumnの表示プロパティにバインドするプロパティをそれぞれ実装する。

        #region inner classes

        public class FileViewModel
        {
            private readonly FileInfo fileInfo;

            public FileViewModel(string path)
            {
                this.fileInfo = new FileInfo(path);
            }

            public string Name 
            { 
                get
                {
                    return this.fileInfo.Name;
                }
            }

            public string LastUpdateDate
            { 
                get
                {
                    return this.fileInfo.LastWriteTime.ToShortDateString();
                }
            }

            public string Type
            { 
                get
                {
                    return this.fileInfo.Extension;
                }
            }

            public string Size
            {
                get
                {
                    return (this.fileInfo.Length / 1000).ToString("#,0K");
                }
            }
        }

        #endregion

Module初期化時にViewを追加

前回と同じように、Moduleの初期化時にViewをRegionに登録する。

WindowsExplorerishModule.cs

    public class WindowsExplorerishModule : IModule
    {
        public void Initialize()
        {
            this.regionManager.RegisterViewWithRegion("MainRegion", typeof(FileListView));
            this.regionManager.RegisterViewWithRegion("LeftRegion", typeof(FolderTreeView));
        }
        // 他は前回と同じため省略
    }

動かすとこんな感じ
somber explorer
一応、フォルダーツリーは、ツリーを展開すると下の階層のフォルダが表示される。
ファイル一覧は同じ表示のままだが。

まとめ

WPFでTreeViewとDataGridを表示した。
前回と同じだが、Moduleの初期化処理でViewを追加した。 特に Prism関係ない・・・

あまりに地味なので、次回もPrismは関係ないけどTreeViewやDataGridにアイコンを表示する予定

これまでの目次

[WPF][Prism] PrismアプリケーションでHello World

はじめに

最初なので、 Prism4のチュートリアル((WPF Hands-On Lab: Get Started with the Prism Library)[英語]を読んで、Hello World程度が動くところまでをやってみる。以下、細かいところは省略するので、詳細は原文を参照のこと。

サンプルの題材としてはベタだけど Windowsエクスプローラ風のアプリケーションを作ることをとりあえずの目標にする。
WPFやMVVMの知識はある程度もっていることが前提で書いていく予定。

準備

Prism v4をダウンロードしてインストール。

メモ:このチュートリアルは Managed Extensibility Framework(MEF)ではなく Unityを使う。

手順

タスク1:Prismを使うソリューションを作る
タスク2:モジュールを追加する
タスク3:Viewを追加する

タスク1:Prismを使うソリューションを作る

  • WPF アプリケーション を作成する(公式チュートリアルと違うが、名前は WpfApplicationと付けた場合の例を以降も使う)
    • これがメインというかエントリポイントというか大枠となるプロジェクトで、他のアセンブリはモジュールとなる。
  • (Visual Studioではなく Windowsエクスプローラでやる作業)作成されたソリューションのディレクトリの直下に Library.Desktop というディレクトリを作成し、以下のDLLをコピーして置く
    • Microsoft.Practices.Prism.dll
    • Microsoft.Practices.Prism.UnityExtensions.dll
    • Microsoft.Practices.ServiceLocation.dll
    • Microsoft.Practices.Unity.dll
    • ※Prismをインストールしたディレクトリの Source\bin\Desktop にあるはず
    • ※XMLドキュメントも同じところにあるので、Intellisenseを効かせたいひとは一緒にコピーしておく
  • これらのアセンブリを参照の追加でWpfApplicationプロジェクトに追加する

Shell

  • ShellとはPrismアプリケーションでの一番親となるウィンドウのこと。
  • WpfApplicationプロジェクトの下にある、MainWindow.xaml をソリューションエクスプローラから Shell.xamlにリネームする。
  • コードビハインドも Shell.xaml.csにリネームされるはず。開いてクラス名がMainWindowのままであればShellに変更する
  • Shell.xamlを開き、<Window x:Class="WpfApplication.Shell"となっていなければ編集してそのようにする

Region

  • Regionとは論理的な画面の区画領域のこと(Gridのような物理的な区画ではない)。たとえばOutlookのようなMUAは、レイアウトをいくつかの選択肢から選べる(左がツリー、上段リスト、下段本文。というものもあれば3列にそれらを表示したりもできる)。よって、メール本文を Gridの2行目に表示という設計をする代わりに、「メール本文表示領域」という論理的な定義をすることができる。
  • Shell.xamlを開き、Window要素に namespace(xmlns:prism="http://www.codeplex.com/prism")を追加する
  • 次のようにRegionを定義する
<Window x:Class="WpfApplication.Shell"
        xmlns:prism="http://www.codeplex.com/prism"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="150*" />
            <ColumnDefinition Width="300*" />
        </Grid.ColumnDefinitions>
        <ContentControl Name="LeftRegion" prism:RegionManager.RegionName="LeftRegion" Grid.Column="0" />
        <ContentControl Name="MainRegion" prism:RegionManager.RegionName="MainRegion" Grid.Column="1" />
    </Grid>
</Window>
  • とりあえず、左側の領域(LeftRegion)と中央のメイン領域(MainRegion)を定義した。Windowsエクスプローラのツリーと一覧のつもり。

Bootstrapper

  • BootstrapperはPrismアプリケーションの初期化を行う、テンプレートメソッドを実装するクラス。今回はUnityBootstrapperを継承して、Unityやモジュールなどの初期設定をする。
  • 新規クラスBootstrapperをWpfApplicationに追加し、次のようにoverrideして、Shellを初期化し表示する
    class Bootstrapper : UnityBootstrapper
    {
        protected override DependencyObject CreateShell()
        {
            return new Shell();
        }

        protected override void InitializeShell()
        {
            base.InitializeShell();

            App.Current.MainWindow = (Window)this.Shell;
            App.Current.MainWindow.Show();
        }
    }
  • App.xaml.csを開き、Bootstrapperを開始するコードを追加する
public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        Bootstrapper bootstrapper = new Bootstrapper();
        bootstrapper.Run();
    }
}
  • App.xamlを開き、StartupUri属性を削除する。PrismではBootstrapperが手動でShell Windowsを表示するため。
<Application x:Class="WpfApplication.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Application.Resources>

    </Application.Resources>
</Application>

タスク2:モジュールを追加する

  • クラスライブラリのプロジェクトを追加する。(原文とは違うがWindowsエクスプローラ風のモジュールということで WindowsExplorerishと名づける)
  • このプロジェクトに参照の追加で、WPF関連のアセンブリを追加する
    • PresentationCore.dll
    • PresentationFramework.dll
    • WindowsBase.dll
    • System.Xaml.dll
  • Prismのアセンブリも追加する
    • Microsoft.Practices.Prism.dll
  • Class1.csをWindowsExplorerishModule.csリネームし、Microsoft.Practices.Prism.Modularity.IModuleインタフェースを実装する
public class WindowsExplorerishModule : IModule
{
        public void Initialize()
        {
            // noop for now
        }
}
  • WpfApplicationプロジェクトのBootstrapper.csに、WindowsExplorerishModuleを取り込むコードを追加する
  • WpfApplicationプロジェクトにWindowsExplorerishModuleプロジェクトの参照を追加する
    class Bootstrapper : UnityBootstrapper
    {
        protected override void ConfigureModuleCatalog()
        {
            base.ConfigureModuleCatalog();

            var moduleCatalog = (ModuleCatalog)this.ModuleCatalog;
            moduleCatalog.AddModule(typeof(WindowsExplorerish.WindowsExplorerishModule));
        }

    // 他は同じ
    }

※このように、メイン側からモジュールを取り込む(依存関係を持つ)ということは、プロジェクトの参照に追加して単に初期化処理を呼び出してもよいわけで従来のライブラリ的な使い方とさほど変わらないが、PrismのModule機能には、OfficeのアドオンやEclipse Pluginのように本体に手を入れずに機能を後付けできるような仕組みがある。それは別の機会に紹介する。

タスク3:Viewを追加する

  • Hello Worldと表示するためのViewをWindowsExplorerishプロジェクトに追加する。
  • Viewsディレクトリを作り、そこに ユーザーコントロールWPFを追加。名前はなんでもよい(デフォルトの UserControl1.xamlとかでOK)
  • Hello Worldという TextBlockを配置
<UserControl x:Class="WindowsExplorerish.Views.UserControl1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <TextBlock Text="Hello World!" />
    </Grid>
</UserControl>
  • 再び WindowsExplorerishModule.csを編集する。モジュールがロードされるとき、IModule.Initialize()が呼び出されるので、初期化処理を追加する。
    • Hello WorldのViewを追加する
public class WindowsExplorerishModule : IModule
{
        private readonly IRegionManager regionManager;

        public WindowsExplorerishModule(IRegionManager regionManager)
        {
            this.regionManager = regionManager;
        }

        public void Initialize()
        {
            this.regionManager.RegisterViewWithRegion("MainRegion", typeof(UserControl1));
        }
}
  • IRegionManager.RegisterViewWithRegion()で、RegionにViewを登録できる。画面の遷移の仕組みの詳細は別の機会に。
  • IRegionManagerが必要であるため、コンストラクタのパラメータで受け取るようなシグネチャに変更した。Unityが Constructor Injectionで設定してくれる。

実行すると、こんな感じ

Hello World on Prism Shell

まとめ

  1. MailWindowはShellと呼ぶ
  2. IUnityBootstrapperのメソッドをオーバライドして、アプリケーションの初期化をする
  3. IModuleの実装クラスにモジュールの初期化処理を実装する
  4. ModuleCatalogにIModuleを登録する。Bootstrapperで行う
  5. 表示する区画をRegionとして定義する。Viewを表示するときはどのRegionに表示するかを指定してRegionManagerに要求する




目次

(1)PrismアプリケーションでHello World

[WPF][Prism] ModularなUI

先日の記事のように、Modularityは大規模開発に対する1つのソリューションだと思う。
サーバサイドだと、ほとんど要件やドメインの構造でモジュールの単位が決まっていくような気がするが、UIだとどうだろう。
単純に画面の区画ごとにモジュール化すればよいわけではない。画面にはメニューバーやコンテキストメニューなど共通的な領域に個別のメニュー項目を配置しなくてはいけなかったり、「コピー」「開く」のような共通的なメニューでも選択されている項目によって実装上の処理を変えなくてはいけなかったりする。
そういったことをフレームワークやライブラリを使ってどのように実現するのか。ということを WPFPrism4を使ってやってみたい。何回かに分けて書く予定。

なお、リッチクライアント(デスクトップアプリケーション)で Modularityを実現するための技術は Eclipse RCPもあるが、Eclipse本体の3.6→3.7 or e4のような進化に引きづられて Platformの APIがすぐに変わっていってしまうというツライ事情がある。

目次

これらの記事は実装をアドホックに追加しながら書いたので、順序がバラバラである。
読み返した時にわかるようにカテゴリに分けて、実装した順番を()カッコに書いてある。