【WPF】WPFのUserControlで、Designer上でダミー画像が表示されるようにする


TL;DR;

<UserControl ... >
    ... 
    <UserControl.DataContext>
        <local:MockData />
    </UserControl.DataContext>
    ... 
</UserControl>

これで利用側は表示され続けます。
しかし、UserControl側はリビルドすると画像だけ消えます。

やりたかったこと

WPFやUWPの利点の一つにXAMLでUIが描けること、そしてVisualStudioではXAMLのデザイナー(グラフィカルエディタ)が搭載されていることがあります。
折角そんな素晴らしい機能があるので、UIの見た目をリアルタイムで確認しながらXAMLを書きたいものです。
しかし、StackPanelなどの自動レイアウトを使っていると、画像が読み込まれているときとそうでないときでImageコントロールのサイズが変わってしまい、デザイナー上での見た目が潰れて大変悲しいことになってしまいます。

これを何とかするために、実行時にBindする予定の部分にも何かしらダミーの画像を当てておいて、ある程度のサイズを確保してくれるようにしたかったのです。

駄目だったパターン

FallbackValueTargetNullValueについて

「xaml Default Designer」みたいなノリで検索すると、BindingBindingBase.FallbackValueBindingBase.TargetNullValueを設定すればいいよ!といった感じの情報が見つかりました。
しかし、TextBlock.Textなどの場合はうまくいくのですが、Image.Sourceについては描いた瞬間は表示されるものの暫く経つor利用側のデザイナーで表示がされなくなってしまいます。

はじめは、Resourcesから供給される画像がBitmapなのとImage.SourceImageSourceの差の問題かとも思ったのですが、適切なConverterをかませても、暫く経つと表示されなくなってしまっていました。

BadSample.xaml
<Image Source="{
    Binding img,
        FallbackValue={StaticResource prop.Resources.fallback_image_icon},
        Converter={StaticResource local:DefaultImageWhenNull}}"/>
<!-- DefaultImageWhenNullは [ValueConversion(typeof(Bitmap), typeof(ImageSource))] なValueConverter -->

Converterで入力がnullの時にデフォルトの画像を返す方法

OSSの実装例をあさっていたところ、以下のようにConverterでデフォルト画像を指定しているところがありました。
ということでこれも試してみたのですが、結果はDataContextがBindされるまでConverterも動かない(FallbackValueがついていても動かない)ということで駄目でした。
実際、元PJを開いてみてもそもそもデザイナ自体表示できなくなっていたので、私のやりたかったことの答えではない意図っぽいです。
たぶん実行時のnullヨケですね。

BadSample2.cs
[ValueConversion(typeof(ImageSource), typeof(ImageSource))]
public class ImageSourceToThumbnailConverter : IValueConverter
{
    static readonly ImageSource _defaultThumbnail = MainWindow.Current.Resources["thumbnail_default"] as ImageSource;
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        => value ?? _defaultThumbnail;
    ...
}

d:DataContextを設定する。

UserControl側

PageItem.xaml
<UserControl
    ...
    d:DataContext="{
        d:DesignInstance local:MockData,
        IsDesignTimeCreatable=True}">
    ...
    <Image Source="{Binding img}" />
    ...
</UserControl>
PageItem.xaml.cs
...
public class MockData
{
    // 返す値はお好みで
    // 今回はプロジェクトプロパティのResource画像から
    public ImageSource img { get; }
        = Imaging.CreateBitmapSourceFromHBitmap(
            Properties.Resources.fallback_image_icon.GetHbitmap(),
            IntPtr.Zero, Int32Rect.Empty,
            BitmapSizeOptions.FromEmptyOptions());
}

利用側

何も考えずに取り込めばOK。

MainWindow.xaml
<Window ... >
    ...
    <local:PageItem />
    <local:PageItem />
    <local:PageItem />
    ...
</Window>

……と思っていたのですが、リビルドをかけた瞬間画像が吹っ飛びました。
どうも、時間が経つと消えるのではなく、リビルドが走ると消えるようです。
というのもリビルドしても、なぜかDataContextに入っているインスタンスがリロードされていない、つまりリビルド以前のインスタンスが入っています。
本当かよって感じですが、GetHashCode をTextBlockにダンプしてみたところ、値は変わりませんでした。
ここからは推測ですが、リビルドをしてアセンブリは切り替わりそれまでのメモリも解放されているのだと思います。
しかし、Image側はそれを握り続けているため解放済みの無効な画像を表示しようとしてもできないくなっていたのではないかと思います。
現に、ImageSourceもダンプしてみましたが、リビルドしてもnullにはなっていませんでした。
意味不明ですね。

参考

Default value at design time XAML(Stackoverflow)
Ito Mitsuhiro/NeeView ImageSourceToThumbnailConverter.cs(BitBucket)
XAMLデザイナ専用ViewModelコンストラクタの作り方(Qiita)