WPFでNumericUpDownを使う

WPFの単項データバインドについて以前ブログに書きましたが、その時NumericUpDownの代わりにSliderを使いました、その理由はWPFにはNumericUpDownが存在しないためでした。そのためテンプレートを駆使してNumericUpDownを自作したり、ライブラリとして公開しているものが見受けられます。

ライブラリを使わず、あまり面倒なこともせず、NumericUpDownを使おうとするとWindowsFormsHostがあります。ただこれにも問題があり、WindowsFormsHostでNumericUpDownをホストすると、NumericUpDownはWindowsFormsのコントロールなのでよく使うValueプロパティが依存関係プロパティでなく、バインディングが効かないため他のWPFコントロール同様に扱えないという問題が生じます。そこで、バインド可能なNumericUpDownをライブラリを使わず、汎用的に作成することを考えてみます。

BindableNumericUpDown.xaml

<WindowsFormsHost x:Class="BindableWinFormsControl.BindableNumericUpDown"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:winform="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
             mc:Ignorable="d"
             d:DesignHeight="30" d:DesignWidth="300">
    <winform:NumericUpDown/>
</WindowsFormsHost>

BindableNumericUpDoiwn.xaml.cs (1) 不完全版

namespace BindableWinFormsControl
{
    using System;
    using System.Windows;
    using System.ComponentModel;
    using System.Windows.Forms;
    using System.Windows.Forms.Integration;
    /// <summary>
    /// BindableNumericUpDown.xaml の相互作用ロジック
    /// </summary>
    public partial class BindableNumericUpDown : WindowsFormsHost
    {
        #region 依存関係プロパティ
        public static readonly DependencyProperty ValueProperty;
        static BindableNumericUpDown()
        {
            BindableNumericUpDown.ValueProperty = DependencyProperty.Register(
                "Value",
                typeof(Decimal),
                typeof(BindableNumericUpDown),
                new FrameworkPropertyMetadata(0m,
                                              FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                                              new PropertyChangedCallback(OnValueChanged)));
        }
        public Decimal Value
        {
            // このプロパティはコンパイル時に参照されるが、実行時には参照されない。
            // そのためこの.NETプロパティラッパにロジックを入れてはいけない。
            get { return (Decimal)GetValue(BindableNumericUpDown.ValueProperty); }
            set { SetValue(BindableNumericUpDown.ValueProperty, value); }
        }
        #endregion

        private static void OnValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
        }
        public BindableNumericUpDown()
        {
            InitializeComponent();
        }
    }
}

WindowsFormsコントロールであるNumericUpDownを使うため、WindowsFormsHostを使うようにしました。ちゃんとバインドできるWPFコントロールとして扱いたいので、依存関係プロパティを定義する必要があります。依存関係プロパティは上を見た頂ければわかるとおり、定義にはstaticフィールドを使うため実行時になんとかして登録するのはダメです。となれば静的に型定義をする必要があり、ジェネリックな定義もできないので上のような基本構成となっています。

さて、これでバインドはできるのですが問題は値が同期していないことです。これは上記コントロールを使った簡単なコードを書くとよくわかります。

 TestBindObject.cs

namespace BindableWinFormsControl
{
    using System.ComponentModel;
    internal class TestBindObject : INotifyPropertyChanged
    {
        private int a;
        public int A {
            get { return a; }
            set {
                a = value;
                NotifyPropertyChanged("A");
            }
        }

        private void NotifyPropertyChanged(string parameter)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(parameter));
        }
        public event PropertyChangedEventHandler PropertyChanged = (s, e) => { };
    }
}

このテストデータをBindableNumericUpDownのValueプロパティとバインドさせたいとしましょう。

 MainWindow.xaml

<Window x:Class="BindableWinFormsControl.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:my="clr-namespace:BindableWinFormsControl"
        Title="MainWindow" Height="350" Width="298.209">
    <StackPanel HorizontalAlignment="Left">
        <Button Height="20" Width="200" Click="ButtonDispClick">内部値の表示</Button>
        <Button Height="20" Width="200" Click="ButtonSetClick" >内部値を100に</Button>
        <my:BindableNumericUpDown Value="{Binding Path=A}" />
    </StackPanel>
</Window>

MainWindow.xaml.cs

namespace BindableWinFormsControl
{
    using System.Windows;
    /// <summary> MainWindow.xaml の相互作用ロジック </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new TestBindObject() { A = 10 };
        }
        private void ButtonDispClick(object sender, RoutedEventArgs e)
        {
            var bindingData = DataContext as TestBindObject;
            if (bindingData != null)
            {
                MessageBox.Show("内部値は : " + bindingData.A.ToString());
            }
        }
        private void ButtonSetClick(object sender, RoutedEventArgs e)
        {
            var bindingData = DataContext as TestBindObject;
            if (bindingData != null)
            {
                bindingData.A = 100;
            }
        }
    }
}

バインドNGの図

MainWindowのコンストラクタで10を設定していますが、0です。これは当然でBindableNumericUpDownコントロールのValueプロパティは10にバインドされていますが、BindableNumericUpDownコントロールのChildであるNumericUpDownコントロールのValueは初期値の0のままだからで、その0が表示されているためです。

ということでBindableNumericUpDownのValueとNumericUpDownのValueを同期させたいのですが、NumericUpDownはINotifyPropertyChangedを実装していないのでGUI上でマウスやキー操作で変更した数値がBindableNumericUpDown.Valueに反映できません。なので、BindableNumericUpDownにINotifyPropertyChangedを実装しましょう。

BindableNumericUpDown.xaml.cs(2) 完成版

namespace BindableWinFormsControl
{
    using System;
    using System.Windows;
    using System.ComponentModel;
    using System.Windows.Forms;
    using System.Windows.Forms.Integration;
    /// <summary>
    /// BindableNumericUpDown.xaml の相互作用ロジック
    /// </summary>
    public partial class BindableNumericUpDown : WindowsFormsHost, INotifyPropertyChanged
    {
        #region 依存関係プロパティ
        public static readonly DependencyProperty ValueProperty;
        static BindableNumericUpDown()
        {
            BindableNumericUpDown.ValueProperty = DependencyProperty.Register(
                "Value",
                typeof(Decimal),
                typeof(BindableNumericUpDown),
                new FrameworkPropertyMetadata(0m,
                                              FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                                              new PropertyChangedCallback(OnValueChanged)));
        }
        public Decimal Value
        {
            // このプロパティはコンパイル時に参照されるが、実行時には参照されない。
            // そのためこの.NETプロパティラッパにロジックを入れてはいけない。
            get { return (Decimal)GetValue(BindableNumericUpDown.ValueProperty); }
            set {
                SetValue(BindableNumericUpDown.ValueProperty, value);
                NotifyPropertyChanged("Value");
            }
        }
        #endregion

        private static void OnValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            BindableNumericUpDown control = sender as BindableNumericUpDown;
            if (control == null) {
                return;
            }
            if (e.Property == ValueProperty)
            {
                control.Value = (Decimal)e.NewValue;
            }
        }

        public BindableNumericUpDown()
        {
            InitializeComponent();
            SetUpBind();
        }

        #region 表示と依存関係プロパティの同期
        /// <summary>
        /// bypassData と NumericUpDown.Value のバインド
        /// </summary>
        private void SetUpBind()
        {
            var binding2 = new BindingSource();
            ((ISupportInitialize)binding2).BeginInit();
            NumericUpDown child = Child as NumericUpDown;
            child.DataBindings.Add(new System.Windows.Forms.Binding("Value", binding2, "Value", true, DataSourceUpdateMode.OnPropertyChanged));
            binding2.DataSource = typeof(BindableNumericUpDown);
            ((ISupportInitialize)binding2).EndInit();
            binding2.DataSource = this;
        }
        private void NotifyPropertyChanged(string propertyName)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        public event PropertyChangedEventHandler PropertyChanged = (s, e) => { };
        #endregion
    }
}

これでちゃんとバインドができました。
バインドOKの図

今回はValueプロパティだけでしたが、ValueChangedイベントもバインドできるかなと。今回の方法ではChild要素は別にNumericUpDownじゃなくて、自作のWindowsFormsコントロールでも良い点を強調したいと思います。

さらにこれを汎用化するT4テンプレートを作る、という構想も膨らんでいますが今回はここまでで。

 

2014/8/7追記

WindowsFormsHostのの拡張方法としてこんな内容がmsdnにありますが、これでもやはりXAMLでバインドできないようなので上記の手法は使い道がありそうです。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中