はじめに
「WPF?よくわからんからコードビハインドで全部済ませればいいんじゃない?(要約)」と言い放ったバ…人が職場にいたので、
なぜWPFで書くのかをDataTriggerの使い方の例とともに書いてみる。
サンプルコード
とりあえず、このような見た目を持つアプリを考えよう。
本の一覧が並んでいて、本の情報には「題名」と「読んだかどうか」を格納している。
この状態のコードは以下の通り。
(簡略化のためPrismを使っているが、大きな影響は無いはず)
Views/MainWindow.xaml
<Window x:Class="BookShelfSample.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
Title="{Binding Title}" Height="300" Width="200" >
<DockPanel>
<ItemsControl ItemsSource="{Binding BookList}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="Gray" BorderThickness="1" Margin="4">
<TextBlock Text="{Binding BookName, Mode=OneTime}" Margin="4"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DockPanel>
</Window>
ViewModel/MainWindowViewModel.cs
using BookShelfSample.Model;
using Prism.Mvvm;
using System.Collections.Generic;
namespace BookShelfSample.ViewModels
{
public class MainWindowViewModel : BindableBase
{
private string _title = "Prism Application";
public string Title
{
get { return _title; }
set { SetProperty(ref _title, value); }
}
public List<BookInfo> BookList { get; set; }
public MainWindowViewModel()
{
BookList = new List<BookInfo>()
{
new BookInfo("1984", true),
new BookInfo("動物農場"),
new BookInfo("すばらしい新世界", true),
};
}
}
}
Model/BookInfo.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace BookShelfSample.Model
{
public class BookInfo
{
public Book book;
public bool hasRead;
public string BookName => book.Name;
public bool HasRead => hasRead;
public BookInfo(string name, bool read=false)
{
book = new Book(name);
hasRead = read;
}
}
public class Book
{
public string Name;
public Book(string name)
{
Name = name;
}
}
}
課題
このアプリをサンプルとしてお客様に提出して、「どのように既読の本を見せるかについて迷っているんです」と伝えてみる。
「枠線の色を赤にしてみたらどうだ」とか、「文字を太字にしよう」とか、色々出てくるだろう。
…そんなとき、どうやって対応する?
良くない例
(コードビハインド以外で)出てくる実装の良くない例は以下のようなものだ。
変更後BookInfo.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Media;
using System.Windows;
namespace BookShelfSample.Model
{
public class BookInfo
{
public Book book;
public bool hasRead;
public string BookName => book.Name;
public bool HasRead => hasRead;
public BookInfo(string name, bool read=false)
{
book = new Book(name);
hasRead = read;
}
public Brush BorderColor
{
get
{
# if CHANGE_BORDER
if (HasRead)
return Brushes.Red;
# endif
return Brushes.Black;
}
}
public FontWeight FontWeight
{
get
{
# if CHANGE_WEIGHT
if (HasRead)
return FontWeights.Bold;
# endif
return FontWeights.Normal;
}
}
}
public class Book
{
public string Name;
public Book(string name)
{
Name = name;
}
}
}
変更後MainWindow.xaml
<Window x:Class="BookShelfSample.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
Title="{Binding Title}" Height="300" Width="200" >
<DockPanel>
<ItemsControl ItemsSource="{Binding BookList}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="{Binding BorderColor}" BorderThickness="1" Margin="4">
<TextBlock Text="{Binding BookName, Mode=OneTime}" FontWeight="{Binding FontWeight}" Margin="4"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DockPanel>
</Window>
どうして良くないのか
まず、概念的な問題として、Model層(BookInfo)が表示に使う__色__や__フォントの太さ__など、
見た目の問題(VMが管理すべきところ)に手を出しているところが問題となる。
また、保守性の面でもこのように一つ一つの変更に対応すると大量のデッドコードが作られる。
修正するにもすり抜けたりして使われないコードが残ることも多いだろう。
クソコードとクソ環境とクソ組織とクソ技術力は独立して起きることは稀のようだし。
では、どうするか。見た目の問題は見た目が対応している部分で解決したい。
それに今回は一個の変数に色々な見た目を紐付けたい。そんな時はDataTriggerを使えば良さそうだ。
良い例
DataTriggerを使って変更に強いコードを作ろう。
色々確認したが、このような作りにすればxmlファイルの差し替えだけで見た目の変更ができそうだ。
リソースの指定位置がWindowではなくDataTemplateなのはここじゃないとこの位置じゃないとバインドが働かなかったから。
Prismのオートバインドに頼っているため、外の方で指定するとバインドの基準がずれるのだろうか。
もちろん、KeyをResourceDictionaryやBorderなどに指定すればもっと親の要素でResourceDictionaryを指定しても問題ない。
アプリケーションレベルで表示を一気に作りを変えたければApp.xmalあたりで指定して、各要素に対応するStyleを割り振るべきだろう。
変更後MainWindow.xaml
<Window x:Class="BookShelfSample.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
Title="{Binding Title}" Height="300" Width="200" >
<DockPanel>
<ItemsControl ItemsSource="{Binding BookList}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<DataTemplate.Resources>
<ResourceDictionary Source="./BorderTypeDictionary.xaml" />
</DataTemplate.Resources>
<Border BorderThickness="1" Margin="4">
<TextBlock Text="{Binding BookName, Mode=OneTime}" Margin="4"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DockPanel>
</Window>
BorderTypeDictionary.xaml(新規追加)
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="Border">
<Setter Property="BorderBrush" Value="Gray"/>
<Style.Triggers>
<DataTrigger Binding="{Binding HasRead}" Value="true">
<Setter Property="BorderBrush" Value="Red"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding HasRead}" Value="true">
<Setter Property="FontWeight" Value="Bold"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
この形式の利点はもちろん、「表示方法を変更するために表示方法を示すファイルだけを編集している」ことだ。
SOLID原則で言うなら単一責任原則、達人プログラマーの言葉を借りるなら「直行性が高い」、
その他表現方法は色々あるが、ざっくり言えばお客様のワガママ要望に対応しやすいので良いコードである。
まとめ
- 一つの値と基準に見た目を変える場合、DataTriggerを検討しよう
- ModelやViewModelに
using System.Windows
を書く前に本当にそのコードが妥当かよく考えよう - 見た目は客の要望次第で特に変わりやすい要素だから、ファイルに書き出すなどして直行性を上げるのがおすすめ
- 「WPFはよくわからない」って言ってコードビハインドで済ませたりはしないようにしよう
- 妥協の産物のクソコードは早いと1習慣で大問題に発展するよ