🏗️

WPFのViewModelをデザイン用と本番用に分離する

に公開

ListViewの表示速度確認用の画面として、下記のような c:\test にある画像をすべて抽出・表示するコードを作成しました。
対象フォルダには3〜5MB程度の画像が約500枚あり、表示にはそれなりに時間がかかります。

しかし、XAMLデザイナーを起動するたびに重い ViewModel を読み込んでしまうため、開発時に非常にストレスになります…!

MainWindow.xaml
<Window x:Class="WPFListViewTest.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:WPFListViewTest.ViewModels"
        Title="Image Viewer" Height="450" Width="800">

    <Window.DataContext>
        <vm:ImageListViewModel />
    </Window.DataContext>

    <Grid>
        <ListView ItemsSource="{Binding Images}"
                  SelectionMode="Single"
                  ScrollViewer.HorizontalScrollBarVisibility="Disabled">
            <ListView.ItemsPanel>
                <ItemsPanelTemplate>
                    <WrapPanel IsItemsHost="True" />
                </ItemsPanelTemplate>
            </ListView.ItemsPanel>
            <ListView.ItemTemplate>
                <DataTemplate>
                    <StackPanel Margin="5">
                        <Image Source="{Binding Image}" Width="100" Height="100"/>
                        <TextBlock Text="{Binding FileName}" HorizontalAlignment="Center"/>
                    </StackPanel>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</Window>
ImageListViewModel.cs
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Windows.Media.Imaging;

namespace WPFListViewTest.ViewModels;

public class ImageListViewModel
{
    public ObservableCollection<ImageItem> Images { get; } = new();

    public ImageListViewModel()
    {
        Initialize();
    }

    public void Initialize()
    {
        string folder = @"c:\test";
        if (Directory.Exists(folder))
        {
            var files = Directory.GetFiles(folder, "*.*")
                .Where(f => f.EndsWith(".jpg") || f.EndsWith(".png") || f.EndsWith(".bmp"));

            foreach (var file in files)
            {
                Images.Add(
                    new ImageItem
                    {
                        FilePath = file,
                        FileName = Path.GetFileName(file),
                        Image = LoadImage(file)
                    });
            }
        }
    }

    private static BitmapSource LoadImage(string path)
    {
        var bytes = File.ReadAllBytes(path);
        using var ms = new MemoryStream(bytes);
        var image = new BitmapImage();
        image.BeginInit();
        image.CacheOption = BitmapCacheOption.OnLoad;
        image.StreamSource = ms;
        image.EndInit();
        image.Freeze();
        return image;
    }
}

public class ImageItem
{
    public string FilePath { get; set; } = string.Empty;
    public string FileName { get; set; } = string.Empty;
    public BitmapSource? Image { get; set; }
}

1. 軽量な ViewModel を用意する

デザイナーの表示が重くなる問題を解消するため、軽量なダミー画像を表示する MockImageListViewModel を作成します。

MockImageListViewModel.cs
using System;
using System.Collections.ObjectModel;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace WPFListViewTest.ViewModels;

public class MockImageListViewModel
{
    public ObservableCollection<ImageItem> Images { get; } = new();

    public MockImageListViewModel()
    {
        Initialize();
    }

    private unsafe static BitmapSource CreateBitmap(int width, int height, uint color)
    {
        var wb = new WriteableBitmap(width, height, 96, 96, PixelFormats.Bgra32, null);
        wb.Lock();
        var span = new Span<uint>((void*)wb.BackBuffer, width * height);
        span.Fill(color);
        wb.Unlock();
        wb.Freeze();
        return wb;
    }

    public void Initialize()
    {
        const int count = 10;
        for (int i = 0; i < count; i++)
        {
            Images.Add(
                new ImageItem
                {
                    FileName = $"image{i}.png",
                    Image = CreateBitmap(100, 100, (uint)Random.Shared.Next() | 0xff000000)
                });
        }
    }
}

2. XAML に軽量ViewModelを読み込む記述を追加

デザイナー上で MockImageListViewModel が表示されるようになります。 ただし、この時点では ImageListViewModel のインスタンスは依然として作成されてしまうため、まだ動作自体は軽量化されていません。

MainWindow.xaml
 <Window x:Class="WPFListViewTest.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:WPFListViewTest.ViewModels"
+       xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+       xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+       mc:Ignorable="d"
        Title="Image Viewer" Height="450" Width="800">

    <Window.DataContext>
        <vm:ImageListViewModel />
    </Window.DataContext>

+    <d:DesignProperties.DataContext>
+        <vm:MockImageListViewModel />
+    </d:DesignProperties.DataContext>

  (以下略)

3. デザイナー起動時の判定処理を追加

デザイナーから起動された場合に ImageListViewModel の初期化処理をスキップするため、デザイナーかどうかを判定するユーティリティを作成します。

WPFUtility.cs
using System.ComponentModel;
using System.Windows;

public class WPFUtility
{
    public static bool IsDesignMode => (bool)DesignerProperties
        .IsInDesignModeProperty
        .GetMetadata(typeof(DependencyObject))
        .DefaultValue;
}

4. 元のViewModelの初期化処理を変更

WPFUtility.IsDesignMode == true の時は、初期化処理をスキップするようにします。

ImageListViewModel.cs
    public ImageListViewModel()
    {
+       if (WPFUtility.IsDesignMode)
+           return;

        Initialize();
    }

これで、デザイナー上では重い初期化処理が実行されず、快適に編集できるようになります! 🎉
もっと楽なやり方があれば、是非教えてください。

実行結果

  • デザイナーでのウィンドウ表示
    デザイナーでのウィンドウ表示

  • 実行時のウィンドウ表示
    実行時のウィンドウ表示

Discussion