[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][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][MVVM] TreeViewのノードを選択状態をdatabindする

はじめに

TreeViewにはSelectedItemプロパティが存在する。ViewModelのプロパティとdatabindすれば、選択されているobjectにアクセスすることができるが、読み取り専用であるため ViewModelからこのプロパティの変更で、TreeView上の選択されている項目を変更することはできない。
そのかわり、 TreeViewのノードである、TreeViewItemにbindされるViewModelオブジェクト(TreeView.ItemsSourceが返す要素たちそれぞれ)とのdatabindを設定すれば、選択状態(IsSelected)を双方向に反映できる。

前回まで作成したTreeViewを例に。

FolderTreeView.xaml

<UserControl 
     : (略)
             >
    <TreeView
             : (略)
                    >
        <TreeView.ItemContainerStyle>
            <Style TargetType="{x:Type TreeViewItem}">
                <Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}"/>
            </Style>
        </TreeView.ItemContainerStyle>
        <TreeView.ItemTemplate>
           : (略)
        </TreeView.ItemTemplate>
    </TreeView>
</UserControl>

FolderTreeViewModel.cs

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

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

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

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

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

FolderTreeView.xamlの一部をもう一度載せる。

<Style TargetType="{x:Type TreeViewItem}">
    <Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}"/>
</Style>

TreeNodeItem.IsSelectedと ViewModelのIsSelectedが双方向(TwoWay)でbindされる。

また、ViewModelには新たにNotificationObjectクラスを継承した。このクラスはプロパティ変更通知を処理するための便利クラスである。
IsSelectedのsetterで変更通知を発行しているのが、RaisePropertyChanged(() => this.IsSelected);である。これにより、INotifyPropertyChanged.PropertyChanged(this, new PropertyChangedEventArgs("IsSelected"));NotificationObjectクラスで実行される。プロパティ名を固定文字列で扱わないため、リファクタリングに強い。

まとめ

TreeViewItem.IsSelectedと、ViewModelは双方向のdatabindが可能。
(応用すると)TreeViewItem.IsExpandedなどのプロパティともdatabindが可能

これまでの目次

[WPF] TreeViewやDataGridにアイコンを表示する

はじめに

前回作成したTreeViewとDataGridにアイコン(イメージ)を表示する。2つの方法を載せる。

  • 静的なイメージを表示
    • プロジェクトにJpgなどの画像ファイルを置いて、それを表示させる方法
  • 動的にイメージを表示
    • プログラムで取得または生成したイメージを表示させる方法

フォルダのツリービューに、静的なアイコンイメージを表示

WindowsExplorerishModuleプロジェクトに Imagesディレクトリを作成し、アイコン用画像(以下の例では Folder.jpeg)を置く。
Folder.jpegのプロパティでビルドアクションをResourceにする
Jpeg Property

FolderTreeView.xamlに<Image />をラベルの横に配置する

<UserControl x:Class="WindowsExplorerish.Views.FolderTreeView">
        <TreeView.ItemTemplate>
            <HierarchicalDataTemplate ItemsSource="{Binding Children}">
                <StackPanel Orientation="Horizontal" Height="20">
                    <Image  />
                    <TextBlock Text="{Binding Label}"/>
                </StackPanel>
            </HierarchicalDataTemplate>
        </TreeView.ItemTemplate>
    </TreeView>
</UserControl>

<Image />タグを選択し、プロパティウィンドウで Sourceプロパティの[…]ボタンをクリックすると、リソースの選択ダイアログが表示されるので、先ほど追加した画像を選択する
SelectJpeg

プロパティウィンドウに戻ると /WindowsExplorerishModule;component/Images/Folder.jpg という文字列が Sourceに設定されているはず。これはpack URIというもので、いつもよく忘れるのでプロパティウィンドウから Visual Studioが設定する値を参考にして思い出すことにしている。

ここで実行するとこのようなツリービューになる

フォルダの種類によってアイコンを変えたい場合は、SourceプロパティとViewModelのstring IconSourceのようなプロパティにdatabindし、応答するURIを切り替えればよい。

FolderTreeView.xaml

    <Image Source="{Binding FolderIconSource}"  />

FolderTreeViewModel.cs

public class TreeNode
{
    // 次のプロパティを追加
    public string FolderIconSource
    {
        get
        {
            return this.IsSpecial ?
                   "/WindowsExplorerishModule;component/Images/SpecialFolder.jpg" :
                   "/WindowsExplorerishModule;component/Images/Folder.jpg";
        }
    }
}

ファイルのグリッドに、動的に生成したアイコンイメージを表示

DataGridの方には、Windowsで「ファイルと関連付けられたアイコン」を表示する。
FileListView.xamlに同じように<Image />タグを配置する。DataGridにImageを含む列を定義する場合は<DataGridTemplateColumn>を使う

<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>
                <DataGridTemplateColumn Header="Name">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <StackPanel Orientation="Horizontal" Height="20">
                                <Image Source="{Binding Icon}" Margin="5,0,5,0" />
                                <TextBlock Grid.Column="1" Text="{Binding Name, Mode=OneWay}" TextAlignment="Left"/>
                            </StackPanel>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
                <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で、今度はstringではなくImageSourceを生成して返す。
System.Drawing.Icon.ExtractAssociatedIcon()から得られたSystem.Drawing.IconBitmapImageに設定する

        public class FileViewModel
        {
            public ImageSource Icon
            {
                get
                {
                    var icon = System.Drawing.Icon.ExtractAssociatedIcon(this.fileInfo.FullName);
                    var stream = new MemoryStream();
                    icon.Save(stream);

                    var bitmapImage = new BitmapImage();
                    bitmapImage.BeginInit();
                    bitmapImage.StreamSource = stream;
                    bitmapImage.EndInit();

                    return bitmapImage;
                }
            }
      }

この実装では MemoryStreamが表示するファイルごとに生成され、開放していない(おそらく自動的にもされない)のでメモリをどんどん食いつぶしていく恐れがある。また、関連付けられたアイコンは拡張子ごとに同じであるため、キャッシュして使いまわすのがよいかと思う。

アプリケーションを起動すると、このように表示される
FolderFileIconized

まとめ

  • 静的なイメージを表示
    • プロジェクトにJpgなどの画像ファイルを置いてビルドアクションをResourceにする
    • Image.SourceにPackURIで画像を指定する
    • 画像ファイルをプログラムで切り替えるには、応答するPackURIをプログラムで切り替えるViewModelを実装する
  • 動的にイメージを表示
    • Image.Sourceにdatabindしたプロパティで、ImageSource(のサブクラス)を応答する
    • 不必要にメモリを消費する恐れがあるので、リソースの管理に注意が必要

これまでの目次