WPF ViewModelでLOB開発 Vol 01 - WPFプロパティ システム

- - posted in wpf | Comments

LOB開発といえば企業が利用する業務システムで、Windows FormとSQLを利用して、あっという間にデータベースと連係して動くアプリケーションを作成できる技術者は多いと思います。

そんなところに出てきたWPF(Windows Presentation Foundation)。
とても優れた技術ですが、これをどうLOB開発として利用するかは少し敷居があるのかと感じています。

『WPF ViewModelでLOB開発』シリーズでは、WPFの恩恵を十分に受けつつ、より簡単に開発するための情報を展開します。

WPFテクノロジー

技術詳細についてはMSDN荒井省三さんのBlog のWPF編を読むことをお勧めします。
ここではVisualStudioを利用しながら動かすことに視点を合わせたいと思います。

ViewModel

ビューモデルと呼び、この連載タイトルの一部にも登場しています。
Model-View-ViewModel というデザイン パターンの中で、画面の状態を持つオブジェクトです。 Wikiを読むと難しく早くも挫折しそうですが、手を動かしながら確認したいと思います。

Visual Studioを起動して SimpleApp というWPFアプリケーションを作成 wpf-01-01 wpf-01-02

自動でxamlが二つ生成されています。

  • App.xaml
  • MainWindowo.xaml
    • メイン画面で、Model-View-ViewModel でいうところの View になります。 今回は中央にSampleTextBoxという名前を付けたテキストボックスを配置しました。
1
 <TextBox Name="SampleTextBox"/>

ここに MainWindow の状態を持つための ViewModel を追加します。

wpf-01-03 wpf-01-04

Textという文字列型のプロパティを持ち、コンストラクタで初期化しています。

1
2
3
4
5
6
7
8
class MainWindowViewModel
{
    public MainWindowViewModel()
    {
        Text = "これはテスト文字列です。";
    }
    public string Text { get; set; }
}

次に MainWindow という ビュー が ビューモデル を利用してデータを表示するための設定を行います。

wpf-01-05

1
2
3
4
5
6
7
8
9
10
11
12
13
/// <summary>
/// MainWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        this.DataContext = new MainWindowViewModel();
        
    }
}

DataContext というプロパティ名が表すように、ここに設定したオブジェクトがデータを表すということになります。
これで ViewModel の設定は完了しました。

ここからさらに各コントロール(ここではテキストボックスだけですが)の データ バインディング を行います。

wpf-01-06

1
this.SampleTextBox.SetBinding(TextBox.TextProperty, new Binding("Text"));

バインディングの方法は様々でXaml(ザムル)といわれる画面を構成するXML上で <TextBox Name="SampleTextBox" Text="{Binding Path=Text}"/> と記述することも可能ですが、ここでは意図してコード上でバインディングしています。

TextBox.TextProperty は依存関係プロパティといい、このバインディングの仕組みはWPF プロパティ システムと呼ばれます。 Bindingクラスのコンストラクタ パラメーターは ViewModel のプロパティ名を設定しています。 この状態でF5キーを押下してデバック実行すると・・・

wpf-01-07

表示されましたね!
フレキシブル レイアウトで、テキストボックスは画面を大きくすると画面と一緒に大きくなります。

画面での入力内容が、DataContext に設定したオブジェクトのプロパティに自動設定されるなどの一連のバインディングの仕組みが用意されており、 この仕組みを利用するのがModel-View-ViewModel デザイン パターンです。

注目するのはMainWindowViewModelクラスで、UIから独立した素のクラスです。
従来の画面とモデルを紐付けるよなグル(接着剤)コードがViewModel上には出てこなくなりました。

LOB開発基盤 - ViewModel

さて、ここまでで発生した「おまじないコード」は・・・

  • WindowのDataContextプロパティへの設定
  • TextBox.TextPropertyという依存関係プロパティを利用したバインディング作業

になります。
まだまだ記述していないおまじないコードもあり、このままではLOB開発に展開できません。

これらのおまじないコードを基底クラスと一連の機能に隠ぺいすることで、最終的にはViewModelで簡単に画面遷移できるようなソリューションになれれば、WPFの恩恵を受けながらLOB開発を進められるかも?!

基盤はこれからのシリーズで少しずつ機能を固めていくので、まずは準備しましょう。 まずは INotifyPropertyChanged インターフェイスを実装したViewModelのベースクラスを準備します。

wpf-01-08

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class BizViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged(string propertyName = null)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
    protected bool SetProperty<T>(ref T storage, T value, 
        [CallerMemberName] string propertyName = null)
    {
        if (object.Equals(storage, value))
            return false;

        storage = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}

ソースコードは荒井省三さんのBlogで紹介されているものを拝借しました。 ポイントは二つで

になります。

INotifyPropertyChanged インターフェイスは、データ バインディングの仕組みの一つで、何かの処理を実行してViewModelのプロパティに値をセットした場合、それをView側に通知する機能です。 この仕組みで、画面上のデータが再描画されます。
プロパティのsetで呼び出すようにします。

CallerMemberName 属性は.Net4.5で新しく出てきた機能で、呼び出し元のメンバー名を自動設定してくれる機能です。 少し前までは、プロパティのsetで OnPropertyChanged("●×プロパティ名") みたいにコーディングしていたのですが、その文字を記述する必要がなくなりました。 .Net4.0以前では利用できませんが、必須な属性ではなく、文字列でプロパティ名をコーディングしていた世界から少しだけタイプセーフになりましたというところです。

こういうおまじないコードをViewModelの基底クラスに準備しておくことにします。

次にバインディングの作業です。 先ほどタイプセーフと呼びましたが、なるべくコンパイラのチェックが働くようなコーディングだとソースコードの量が増えたときに便利です。 そこで、バインディングのときにコーディングした

1
new Binding("Text")

ここの”Text”というプロパティ名もタイプセーフにセットできるようにしたいと思います。

これはWPFとは関係のない、ただの技術情報なのですが、ラムダというコーディングの記述方法があります。
LINQが利用されるようになってきて、このラムダにおける型の推論によるコーディング方法も浸透してきたと思います。

1
2
//LINQというのは、こんな感じ
IEnumerable<Customer> customers = customers.Where(c => c.City == "London");

C#の経験があるといってもラムダを初めてみる人は、 c => c.City == "London" の記述を見るとびっくりするかもしれませんが、VisualStudioでインテリセンスが働くので、すぐに慣れると思います。

このラムダを利用して、タイプセーフにViewModelのプロパティ名を取得するような簡単なユーティリティを準備します。 こういうのはすでに世の中に出回っているので、探せば出てきます

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static class ExpressionExtensions
{
    public static string ToPropertyName<T>(this T obj, Expression<Func<T, object>> expr)
    {
        return expr.ToSymbol();
    }
    public static string ToSymbol(this Expression expr)
    {
        if (expr == null)
            return null;

        var memExp = (expr as LambdaExpression).Body as MemberExpression;
        var list = new List<string>();
        while (memExp is MemberExpression)
        {
            list.Add(memExp.Member.Name);
            memExp = memExp.Expression as MemberExpression;
        }
        return string.Join(".", list.Reverse<string>());
    }
}

あとは MainWindowViewModel のベースクラスを BizViewModel として

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MainWindowViewModel : BizViewModel
{
    public MainWindowViewModel()
    {
        Text = "これはテスト文字列です。";
    }
    public string Text
    {
        get { return text; }
        set
        {
            SetProperty(ref text, value);
        }
    }
    private string text; 
}

さっきのユーティリティを利用すると

wpf-01-09

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// <summary>
/// MainWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        var viewModel = new MainWindowViewModel();
        this.DataContext = viewModel;

        var textPropertyName = viewModel.ToPropertyName(x => x.Text);
        this.SampleTextBox.SetBinding(TextBox.TextProperty, new Binding(textPropertyName));
    }
}

これでコンパイラでチェックがかかるタイプセーフなWPFの簡単な基盤ができました。 イベント系などは次回紹介するとして、今はViewおよびViewModelの役割を設定していきたいと思います。

LOB開発基盤 - View

次にViewについて少し掘り下げていきます。 今はWindowのXamlに直接記述しています。

1
2
3
4
5
6
7
8
<Window x:Class="SimpleApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <TextBox Name="SampleTextBox"/>
    </Grid>
</Window>

Windowタグの中には一つのコンテンツのみを配置できます。
今は <Grid> が配置されています。コードで記述すると

1
2
3
4
5
6
7
8
9
10
/// <summary>
/// MainWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        this.Content = new Grid();

なので、コンテンツは一つのみとなります。 ただし、このコンテンツがどのように描画されるかはWPFの拡張機能によって変わってきます。

試しにデバック実行し、WPFビジュアライザーで確認してみると

wpf-01-10

MainWindowの中にあるContentPresenterにGridが配置されています。
別の機会に詳しく紹介しますが、WPFのStyleが適用されている結果このような表示になります。

少し前までAero.NormalColor.xamlなどスタイルがMSDNからダウンロードできたのですが、リンクが見つかりませんでした。 現在のデバック実行した環境でWindowクラスに適用されているスタイルは、以下のようになっているのだと思います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<Style x:Key="{x:Type Window}"
       TargetType="{x:Type Window}">
    <Setter Property="Foreground"
            Value="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}"/>
    <Setter Property="Background"
            Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type Window}">
                <Border Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    <AdornerDecorator>
                        <ContentPresenter/>
                    </AdornerDecorator>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>

TemplateというWPFの外観をカスタマイズできる機能があり、ここにコンテンツを表示する際はBorderを用意して、AdornerDecoratorを用意してその中にContentPresenterを・・・という風に定義されています。

話を戻すと、Windowに表示されるコンテンツはContentPresenterを利用して表示されるようになっています。

MSDNのContentPresenterをみると、コンテンツがどのようなロジックで表示するかの記述があります。 たとえば、コンテンツに文字列を設定するとTextBlockが自動で作成されて、表示されるようになります。

wpf-01-11

wpf-01-12

F5キーでデバック実行

wpf-01-13

任意のクラスの場合はToStringの結果が表示されるのですが、その場合に何を表示すべきかをカスタマイズする機能があります。

試しにContentTemplateSelectorプロパティに設定して動作確認してみましょう。

wpf-01-14

戻り値のDataTemplateはビジュアル要素(つまりView)をあらわし、引数のitemは任意のデータをあらわします。
この仕組みの面白いところは、データがビジュアル要素を決定するというところです。 つまり、ビジュアル要素はデータの属性として表現すると、仕組みを上手に利用できそうだとわかります。

さっそくオブジェクトとしてそれを表現してみたいと思います。
ビジュアル要素はデータの属性として扱うので

1
2
3
4
5
6
7
8
public class VisualElementsAttribute : Attribute
{
    public VisualElementsAttribute(Type visualType)
    {
        VisualType = visualType;
    }
    public Type VisualType { get; private set; }
}

この属性はデータ(BizViewModelから派生した任意のViewModel)に付与することになります。
次にビジュアル要素(ビュー)用の基底クラスを用意します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class BizView : UserControl
{
    protected BizView()
    {
        DataContextChanged += OnBizViewDataContextChanged;
    }
    private void OnBizViewDataContextChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e)
    {
        BuildBinding();
    }
    protected virtual void BuildBinding()
    {

    }
}

上記二つを利用して、BizViewModelに肉付けします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public abstract class BizViewModel : INotifyPropertyChanged
{
    protected BizViewModel()
    {
        if (VisualElements != null)
        {
            View = Activator.CreateInstance(VisualElements.VisualType) as BizView;
        }
    }
    private VisualElementsAttribute visualElements;
    public VisualElementsAttribute VisualElements
    {
        get
        {
            if (visualElements == null)
            {
                visualElements = this.GetType()
                                     .GetCustomAttributes(typeof(VisualElementsAttribute), false)
                                     .FirstOrDefault() as VisualElementsAttribute;
            }
            return visualElements;
        }
    }
    private BizView view;
    public BizView View
    {
        get { return view; }
        set
        {
            view = value;
            view.DataContext = this;
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged(string propertyName = null)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
    protected bool SetProperty<T>(ref T storage, T value,
        [CallerMemberName] string propertyName = null)
    {
        if (object.Equals(storage, value))
            return false;

        storage = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}

先ほどDataTemplateSelectorを派生した動作確認用のサンプルを作成しましたが、正しくビジュアル要素を返すDataTemplateSelectorを作成します。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ViewDataTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        var vm = item as BizViewModel;
        if (vm == null || vm.VisualElements == null)
            return base.SelectTemplate(item, container);

        var template = new DataTemplate() { VisualTree = new FrameworkElementFactory(vm.VisualElements.VisualType) };
        template.Seal();
        return template;
    }
}

ここまででのLOB開発用の基盤は以下のようになっています。

wpf-01-15

これを早速使って画面を表示したいと思います。

LOB開発基盤 - はじめの一歩

コンテンツもViewModelも何もない空のMainWindowのみの状態です。

wpf-01-16

ここにViewを追加します。

wpf-01-17

名前はMainViewで、WPFのユーザーコントロールで追加します。
VisualStudioにはItemTemplatesフォルダがあって、これを利用すると便利なのですが、それは次回に紹介します。

MainViewクラスのベースクラスをBizViewに変更します。

wpf-01-18

この作業に今は手間がかかりますが、ItemTemplateを紹介するまでの我慢です。

MainView.xamlに先ほど作成した基盤の名前空間をインポートxmlns:uen="clr-namespace:uEN.UI;assembly=uEN"して、メインのタグを<uen:BizViewに変更します。
合わせてMainView.xaml.csのベースクラスもBizViewに変更します。

このViewに先ほどと同じようにテキストボックスを配置します。

wpf-01-19

次にMainViewModelを追加します。

wpf-01-20

1
2
3
4
5
6
7
8
9
[VisualElements(typeof(MainView))]
public class MainViewModel : BizViewModel
{
    public MainViewModel()
    {
        MyProperty = "ViewModelでLob開発";
    }
    public string MyProperty { get; set; }
}

このViewModelをMainViewのBuildBindingでデータバインディングします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// <summary>
/// MainView.xaml の相互作用ロジック
/// </summary>
public partial class MainView : BizView
{
    public MainView()
    {
        InitializeComponent();
    }

    protected override void BuildBinding()
    {
        var viewModel = this.DataContext as MainViewModel;
        var propertyName = viewModel.ToPropertyName(x => x.MyProperty);
        this.SampleTextBox.SetBinding(TextBox.TextProperty, new Binding(propertyName));
    }
}

今は空のMainWindowとMainView/MainViewModelがある状態です。

wpf-01-21

最後にMainWindowにコンテンツとセレクターを設定します。

1
2
3
4
5
6
7
8
9
10
11
12
/// <summary>
/// MainWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.Content = new MainViewModel();
        this.ContentTemplateSelector = new ViewDataTemplateSelector();
    }
}

以上で準備が完了です。
デバック実行すると・・・

wpf-01-22

期待した結果が表示されました!

WPFの「おまじないコード」などを基盤に入れていくと、それを利用するLOB開発は楽になると思います。
たとえば

  • BizViewModelにShowメソッドを用意し、WindowをNewするコードを入れれば・・・
  • コマンドのバインディングのラッパーを・・・
  • ロギングが・・・

思いつくままをオレオレ実装してもいいですし、Application Architecture Guideなどを読み解いて、しかるべき処理を準備していくのも一つかもしれません。

次回は今までの作業で手間だったものの自動化を考えてItemTemplatesの紹介と、もう少し踏み込んでボタンを押して動くところまでもっていければと思います。

今回のサンプルはコチラ


Comments