#概要
XAMLデザイナでデザイナ用インスタンスを指定すると、実行時の結果が表示されるので、デザイン作業が捗ります。
しかし、デザイン用インスタンスには制限があるので、実際に使用するViewModelは使えないことがあります。
そこでデザイナ専用のViewModelコンストラクタを別途作ります。
そして、デザイナ以外からのこのコンストラクタ呼び出しを防止します。
#変更前
こんなアプリを題材にします。
実行すると、ファイルから文字列を読み込んで表示するだけのソフトです。
今回は話に関係ないのでINotifyPropertyChangedは実装していません。
##View
View層はWindowの中心にUserControl、その中心にTextBlockが置いてあるだけです。
<Window x:Class="WpfDesignOnlyConstruter.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfDesignOnlyConstruter"
Background="SkyBlue"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<local:MyUserControl DataContext="{Binding MyUserControlVM}" Margin="100"/>
</Window>
<UserControl x:Class="WpfDesignOnlyConstruter.MyUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfDesignOnlyConstruter"
Background="Coral"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
d:DataContext="{d:DesignInstance {x:Type local:MyUserControlViewModel},IsDesignTimeCreatable=True}">
<TextBlock Text="{Binding Text}" VerticalAlignment="Center" HorizontalAlignment="Center"/>
</UserControl>
##Model
Model層では生成時に指定されたファイルパスを読み取り、プロパティに設定します。
public MyModel(string path)
{
this.Text = File.ReadAllText(path);
}
##ViewModel
ViewModel層ではMainWindowViewModelでModel・子ViewModelの生成をしています。
public MainWindowViewModel()
{
this.model = new MyModel("input.txt");
this.MyUserControlVM = new MyUserControlViewModel(model);
}
子ViewModelではModelのファイルから読まれた文字を受け取って表示用プロパティに設定しています。
public string Text { get; set; }
public MyUserControlViewModel(MyModel model)
{
this.Text = model.Text;
}
#問題点
実行時の動作には問題ありませんが、XAMLデザイナーで結果が表示されません。
ユーザーコントロールのXAMLデザイナーですが、真ん中にあるはずの文字が表示されません。
d:DataContext="{d:DesignInstance {x:Type local:MyUserControlViewModel},IsDesignTimeCreatable=True}"
の部分でデザイン用インスタンスとしてViewModelを生成しています。
しかしViewModelに引数なしのコンストラクタが無いとXAMLデザイナーに結果が表示されません。
また引数なしコンストラクタがあっても内部でファイルアクセスなどを実行していると、やはりXAMLデザイナーに結果は表示されません。
この状態ですと実行時と違うので、デザインがしづらいです。
清く正しい解決策は
d:DataContextでデザイナ用にViewModelを設定する
のようにデザイナ用データをXAMLで作って渡すことです。
#解決方法
しかし、膨大な表示項目があった場合、ViewModelのダミーをデザイナのためだけに作るのは正直めんどうです。
そこで簡単に済ませるために、ViewModelに(必要であればModelにも)デザイナー用のコンストラクタを作ってしまいます。
/// <summary>
/// !!!!XAMLデザイナー用!!!!呼び出しダメ。ゼッタイ。
/// </summary>
public MyUserControlViewModel() : this(new MyModel()) { }
/// <summary>
/// !!!!XAMLデザイナー用!!!!呼び出しダメ。ゼッタイ。
/// </summary>
public MyModel()
{
this.Text = "Designer only text";
}
こうして、XAMLデザイナ専用の引数なしViewModelコンストラクタを作ることで、XAMLデザイナに結果が表示できました。
#新しい問題
しかしXAMLデザイナー以外からもこのコンストラクタは呼び出せます。
どんなにヘルパーテキストで警告したところで、読まない人は読みません。
#新しい解決方法
そこでObsolete属性を使用してコードからの呼び出しを防ぎます。
XAMLデザイナー用のViewModelコンストラクタをこうします。
/// <summary>
/// デザイナー用です コードからは呼べません
/// </summary>
[Obsolete("Designer only", true)]
public MyUserControlViewModel() : this(new MyModel()) { }
呼び出すとコンパイルエラーになります。警告文も平和になりました。
第2引数をtrueにしてObsolete属性をマークすると、コードから呼ばれた際にコンパイルエラーを起こします。
ただし、Obsolete属性がついたメソッド内でObsolete属性がついたコードを呼ぶことは出来ます。
つまりデザイナ専用viewModelコンストラクタでデザイナ専用Modelコンストラクタを呼ぶことは問題ありません。
コードから呼べないObsolete属性ですが、XAMLデザイナーはこれを無視します。
ですのでXAMLデザイナー側は何も変える必要はありません。
めでたしめでたし。
#注意点
- 廃止属性なのに廃止されない
- デザイナ限定属性などにしたいが、廃止属性は継承できず、他の方法も見つからない。
- デザイナーのためのコードがViewModelやModelにあるのはいかがなものか。
- XAMLデザイナーがObsoleteAttributeを無視するのが仕様なのか解らないので、
今後のアップデートなどで変わるかも。 - 邪道感ある
#環境
VisualStudio2015
VisualStudio2017
.NET Framework 4.6
C#6