こんにちは、働くC#プログラマーのさんさめです。
WPFで開発を進めていくと、
コントロールを使いまわしたくなるケースが生じてきます。
- 異なるビューで共通の見た目を使いたくなった
- 定型文的に配置している組み合わせがある
- 似たようなxamlを打つのがしんどくなってきた
WPFにおいてコントロールを使いまわしたくなった時、
もっとも手軽に使いまわせるようにする方法は
ユーザーコントロールとして切り出すことです。
しかし、様々な事情により、
一度ユーザーコントロールとして作ってしまったものの、
カスタムコントロールとして
作り直したくなるケースがありました。
さんさめのケースでは
大量に配置されるので処理コストが気になってきた
しかし、何も考えずにプロジェクトを作っていたため、
カスタムコントロール化するために、
かなりの手順を踏む必要がありました。
本記事では、
「既存のユーザーコントロールを
カスタムコントロールに変更したい!」
という人に向けて、ユーザーコントロールを
カスタムコントロールに変えるやり方を解説します。
ユーザーコントロールならどうなるのか
さて、手順を解説するために、
まずはコントロールを使いまわしたくなったところから
シミュレートしていきましょう。
ここでは、
「ボタンと説明文をセットにしたコントロール」
を使いまわしたくなったとします。
ファーストステップとしては、
やはりユーザーコントロールを作るのが手早いです。
以下のようなユーザーコントロールになったとします。
<UserControl x:Class="WpfCustomControlLibrary1.ButtonWithComment"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfCustomControlLibrary1">
<DockPanel>
<TextBlock DockPanel.Dock="Right"
Text="{Binding Comment, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:ButtonWithComment}}}"/>
<Button Content="{Binding ButtonText, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:ButtonWithComment}}}"
Command="{Binding Command, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:ButtonWithComment}}}"/>
</DockPanel>
</UserControl>
Buttonがメインで、
TextBlockが右側にあるような配置です。
コードビハインドは以下の通りです。
public partial class ButtonWithComment : UserControl
{
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
// Using a DependencyProperty as the backing store for Command. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register("Command", typeof(ICommand), typeof(ButtonWithComment), new PropertyMetadata(null));
public string Comment
{
get { return (string)GetValue(CommentProperty); }
set { SetValue(CommentProperty, value); }
}
// Using a DependencyProperty as the backing store for Comment. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CommentProperty =
DependencyProperty.Register("Comment", typeof(string), typeof(ButtonWithComment), new PropertyMetadata(string.Empty));
public string ButtonText
{
get { return (string)GetValue(ButtonTextProperty); }
set { SetValue(ButtonTextProperty, value); }
}
// Using a DependencyProperty as the backing store for ButtonText. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ButtonTextProperty =
DependencyProperty.Register("ButtonText", typeof(string), typeof(ButtonWithComment), new PropertyMetadata(string.Empty));
public ButtonWithComment()
{
InitializeComponent();
}
}
Bindingを可能にするために、
3つほど依存プロパティを追加しています。
それ以外の処理は特に行っていません。
実際に配置してみて、
ちゃんと使いまわせているかどうか確認してみましょう。
サンプルコードを載せます。
<Window x:Class="WpfCustomControlLibrary1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfCustomControlLibrary1"
Title="MainWindow" Height="450" Width="800">
<DockPanel>
<local:ButtonWithComment ButtonText="終了"
Comment="保存せずに終了します"
DockPanel.Dock="Bottom"
HorizontalAlignment="Center"/>
<local:ButtonWithComment ButtonText="保存"
Comment="編集内容を保存します"
DockPanel.Dock="Bottom"
HorizontalAlignment="Center"/>
<StackPanel>
<TextBlock Text="Hogeプロパティ"/>
<TextBox Text="ほげ"/>
<TextBlock Text="Fugaプロパティ"/>
<TextBox Text="ふが"/>
</StackPanel>
</DockPanel>
</Window>
実行した結果は以下のようになります。
ちゃんとコメント付きボタンが2つ表示されていますね。
(中央寄せなのでレイアウトが残念ですが)
さて、前提条件を整えたところで、
いよいよ本題です。
このコントロールを実際に
カスタムコントロールに変えていきたいと思います。
カスタムコントロールを作る準備
カスタムコントロールを作るためには、
そのコントロール自体を編集するだけでなく、
プロジェクトの設定にも変更を加える必要があります。
(※最初から『カスタムコントロールライブラリ』で
プロジェクトを作っている場合は不要です)
準備その1:ThemeInfo属性の設定
まずは、プロジェクト内にあるAssemblyInfo.csを開いてください。
…いや、そんなファイル無いけど。
まさかさっそく悩む羽目に…?
…はい、しれっと「開いてください」
と書きましたが、
.csprojが新形式仕様の場合、
AssemblyInfo.csは自動生成されないので、
まだ存在しません。
その場合は、AssemblyInfo.csを新規作成してください。
プロジェクト直下でも、
「Properties」フォルダを掘っても構いません。
旧形式のcsprojだと、
Propertiesフォルダを掘ったところに生成されるのがデフォルトですね。
そして、中身に以下のように書きます。
using System.Windows;
[assembly: ThemeInfo(ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly)]
AssemblyInfo.csが最初からあった場合は、
ファイルの一番下などに、
ThemeInfo属性の方だけを追記してください。
(※「using System.Windows;」はおそらくすでにあると思います。
なければ足してください)
準備その2:Generic.xamlの作成
さて、次は「Generic.xaml」を作りましょう。
まず、「Themes」フォルダをプロジェクト直下に作り、
次に「Generic.xaml」というファイル(リソースディクショナリ)を
そのフォルダの中に作成します。
ソリューションエクスプローラで見た時に、
以下のような見た目になってればOKです。
そして、Generic.xamlの中には色々記述していくことになるのですが、
ひとまずは以下のように空のResourceDictionaryとしておいてください。
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfCustomControlLibrary1">
</ResourceDictionary>
さて、これでカスタムコントロールを作る準備が整いました。
ここら辺で一度ビルドしてみて、
正常にビルドが終了するかチェックしておきましょう。
ここでビルド失敗するようなら、
既に何かおかしいです。
今までの手順を見直してみてください。
では次に、既存のユーザーコントロールに手を加えていきます。
コードビハインドの変更。継承元を変えてpartialを外す
コードビハインドにおける変更は主に3点です。
- 継承元をUserControlからControlに変更する
- クラス宣言部分からpartialを外す
- デフォルトスタイルが当たるようにメタデータを設定する
- コンストラクタのInitializeComponentを削除
先ほどのコードビハインドを例に、
実際に手を加えていきましょう。
継承元をUserControlからControlに変更する
まず、UserControlではなくなるので、
UserControl継承である必要が無くなります。
とりあえずControl継承にしておきましょう。
実は、今回の例であれば、Button継承などの方が
見通しやカスタマイズ性の面で適格なのですが、
本筋から逸れてしまうので割愛します。
クラス宣言部分からpartialを外す
xamlとセットではなくなるので、
partial修飾子を削ります。
デフォルトスタイルが当たるようにメタデータを設定する
これは少しややこしいですね。
staticコンストラクタを作り、
そこでDefaultStyleKeyProperty.OverrideMetadataメソッドを呼び出します。
ほとんどおまじないみたいなものなので、
以下を参考に設定してください。
static ButtonWithComment()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ButtonWithComment), new FrameworkPropertyMetadata(typeof(ButtonWithComment)));
}
コンストラクタのInitializeComponentを削除
カスタムコントロールではInitializeComponentは不要です。
コンストラクタごと削除してしまいましょう。
コンストラクタで何かやっていた場合は、
コンストラクタ自体は残しておいてもいいですが、
カスタムコントロールの場合は
OnApplyTemplateメソッドでやるべきかもしれませんね。
あれー…。
なんかエラーの波線出まくっているけど…?
ここまで終わってみても、
明らかにビルドが通っていないと思います。
それもそのはず、
まだ同名の.xamlが残っており、
「partial」修飾子付き同名クラスが、
プロジェクト内に存在するのです。
というわけで次は、
その.xamlを削除する工程です。
これが終われば全工程完了です。あとちょっと!
レイアウトの変更。既存xamlは削除しGeneric.xamlに移植
さて、.xamlをいきなり削除する前に、
カスタムコントロールの準備で作った
Generic.xamlを作りこんでいきます。
カスタムコントロールではGeneric.xamlに
レイアウトなどを作りこんでいきます。
というわけで、移植してみたGeneric.xamlが以下になります。
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfCustomControlLibrary1">
<Style TargetType="{x:Type local:ButtonWithComment}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<DockPanel>
<TextBlock DockPanel.Dock="Right"
Text="{Binding Comment, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:ButtonWithComment}}}"/>
<Button Content="{Binding ButtonText, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:ButtonWithComment}}}"
Command="{Binding Command, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:ButtonWithComment}}}"/>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
やっていることは、
StyleによるTemplateプロパティの上書きですね。
「ControlTemplate」タグの内部が、
移植元のxamlに書いてあった内容そのままとなっています。
最後に、元々の.xamlを削除します。
VisualStudio上で削除すると、
コードビハインド(だったもの)の.csも
巻き込まれて消えてしまう可能性があるので、
念のためエクスプローラ上で削除します。
カスタムコントロール化完了。動作確認する
さて、これで晴れてカスタムコントロール化の工程は完了しました!
さっそくビルドして実行してみましょう。
ちゃんと実行できましたし、
最初の実行結果と同じ見た目になりました。
ここまでくれば、
あとは通常のカスタムコントロールを編集する手順に
載ることができます。
よりカスタマイズ性を高めても良し、
シンプルなビジュアル構造にして
ビジュアルツリーの負荷を低減するも良し、
快適なカスタムコントロール作成ライフを満喫しましょう!
まとめ
まとめです。
- コントロールを使いまわしたいなら、
ユーザーコントロールかカスタムコントロール - 一度作ってしまったユーザーコントロールを
カスタムコントロールに移植するのは意外と手間 - ユーザーコントロールが
カスタムコントロール化されるまでの工程を解説
最後までお読みいただき、ありがとうございました。
コメント