Microsoft Solver Foundationで最適化問題を解く

Microsoft Solver FoundationというライブラリがMSDN Archiveからダウンロードできることをご存知でしょうか。今回は、Excelからも使えるこのライブラリをご紹介します。

install

まず上記ダウンロードページからExpressEditionをインストール、NextとAgree連打でOKです。注意点として、このライブラリを使う場合、.NET Framework4.0が必要です。(.NET 4.0ClientProfileも、.NET4.5もダメです)

解くべき問題は以下を想定します。ここを参考にさせてもらいました。

問題

問題2

このレベルならx, yと置いてXY平面上に制約を直線で引いて解いてもいいですが、つまらんのでC#のConsoleアプリケーションとして解きましょうというお話。Microsoft.SolverFoundationライブラリを参照設定追加しておきます。

namespace LinearProgramming
{
    using System;
    using Microsoft.SolverFoundation.Solvers;

    class Program
    {
        static void Main(string[] args)
        {
            // 線形計画法のソルバー
            var solver = new SimplexSolver();

            // 答えは製品1を[x1]コ、製品2を[x2]コとする。
            // それぞれの変域制限も可能、ここでは0-100で設定しておく。
            int x1, x2;
            solver.AddVariable("製品1", out x1);
            solver.SetBounds(x1, 0, 100);
            solver.AddVariable("製品2", out x2);
            solver.SetBounds(x2, 0, 100);

            // 製品1, 2の特徴である原材料A, Bと価格の行を作成。
            int zairyouA, zairyouB, price;
            solver.AddRow("材料A", out zairyouA);
            solver.AddRow("材料B", out zairyouB);
            solver.AddRow("価格", out price);
            // 材料A: 製品1は1kg使う、製品2は1kg使う、トータル4kgまで。
            solver.SetCoefficient(zairyouA, x1, 1);
            solver.SetCoefficient(zairyouA, x2, 1);
            solver.SetBounds(zairyouA, 0, 4);
            // 材料B: 製品1は3kg使う、製品2は1kg使う、トータル6kgまで。
            solver.SetCoefficient(zairyouB, x1, 3);
            solver.SetCoefficient(zairyouB, x2, 1);
            solver.SetBounds(zairyouB, 0, 6);

            // 価格(売上)を最大化するのが目的なのでAddGoalにfalseを指定する。
            // 最小化したい場合はtrueを指定
            solver.SetCoefficient(price, x1, 8);
            solver.SetCoefficient(price, x2, 6);
            solver.AddGoal(price, 1, false);

            // 解表示
            solver.Solve(new SimplexSolverParams());
            Console.WriteLine("製品1:{0}コ, 製品2:{1}コ", solver.GetValue(x1).ToDouble(), solver.GetValue(x2).ToDouble());
            Console.WriteLine("価格:{0}万円", solver.GetValue(price).ToDouble());
            Console.WriteLine("原材料A:{0}kg, 原材料B{1}kg", solver.GetValue(zairyouA).ToDouble(), solver.GetValue(zairyouB).ToDouble());
        }
    }
}

result

このライブラリExpressEditionでも最大8コア, 8GBRAM想定のマルチスレッド対応しており、線形計画法以外に非線形の問題にも対応している模様。再頒布可能パッケージはなく、大学や研究者向けのようですが企業でもExcelアドインとして使えるので評価解析ソフトとして組込む価値があるかと思います。

C#でMouseWheelのフック

前回Ctrl+マウスホイールで拡大縮小するImageコントロールを用意しました。

そのホイール、WPFならルーティングイベントでどうにかなりそうですが、WindowsFormsでは対象コントロールがフォーカスを持っていないとホイールイベントは取得できません。そこで”フック”が登場します、フックとはWindowsから各アプリケーションに送られてくるWINDOWメッセージをのぞき見たり、時には上書きしたり無かったことにしたりするための仕組みです。

C#でのフックの仕方はココにやり方が載っています。、やってみるとわかりますがWH_MOUSEではマウス押下は取得可能ですが、マウスホイールはフックできません!またページ下部に記載ありますが、.NET Frameworkでは他のスレッドのメッセージもフックできる “グローバルフック” はサポートされておらず、自スレッドのメッセージだけフックできる “ローカルフック” がサポートされているとのこと。そもそも”フック”ってこんなにいろんな種類があるんですね。

じゃあpureC#のWindowsFormsで、他のコントロールにフォーカスがある場合マウスホイールは取得できないのか?いいえ、できます。

1.フックを使う方法

めげずにフックです。先ほどのMSDNのサンプルを変更して作ります。

csharpmousehook

namespace MouseWheelHook
{
    using System;
    using System.Windows.Forms;
    using System.Runtime.InteropServices;

    public partial class MouseHookForm : Form
    {
        #region PInvoke
        public delegate int HookProc(int nCode, IntPtr wParam, IntPtr lParam);

        static int hHook = 0;
        static int hLLHook = 0;

        public const int HC_ACTION = 0;
        public const int WH_MOUSE = 7;
        public const int WH_MOUSE_LL = 14;
        public const int WM_MOUSE_WHEEL = 0x20A;

        HookProc MouseHookProcedure;
        HookProc MouseHookLLProcedure;

        [StructLayout(LayoutKind.Sequential)]
        public class POINT
        {
            public int x;
            public int y;
        }

        [StructLayout(LayoutKind.Sequential)]
        public class MouseHookStruct
        {
            public POINT pt;
            public int hwnd;
            public int wHitTestCode;
            public int dwExtraInfo;
        }

        [StructLayout(LayoutKind.Sequential)]
        public class MouseLLHookStruct
        {
            public POINT pt;
            public int mouseData;
            public int flags;
            public int time;
            public int dwExtraInfo;
        }

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
        public static extern IntPtr GetModuleHandle(string lpModuleName);

        [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
        public static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hInstance, int threadId);

        [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
        public static extern bool UnhookWindowsHookEx(int idHook);

        [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
        public static extern int CallNextHookEx(int idHook, int nCode, IntPtr wParam, IntPtr lParam);
        #endregion

        public MouseHookForm()
        {
            InitializeComponent();
        }

        private void buttonMouseHook_Click(object sender, EventArgs e)
        {
            if (hHook == 0)
            {
                // Create an instance of HookProc.
                MouseHookProcedure = new HookProc(MouseHookForm.MouseHookProc);

                hHook = SetWindowsHookEx(WH_MOUSE,
                            MouseHookProcedure,
                            (IntPtr)0,
                            AppDomain.GetCurrentThreadId());
                //If the SetWindowsHookEx function fails.
                if (hHook == 0)
                {
                    MessageBox.Show("SetWindowsHookEx Failed");
                    return;
                }
                buttonMouseHook.Text = "UnHook Windows Hook";
            }
            else
            {
                bool ret = UnhookWindowsHookEx(hHook);
                //If the UnhookWindowsHookEx function fails.
                if (ret == false)
                {
                    MessageBox.Show("UnhookWindowsHookEx Failed");
                    return;
                }
                hHook = 0;
                buttonMouseHook.Text = "Set Windows Hook";
                this.Text = "Mouse Hook";
            }
        }
        private void buttonMouseLowLevelHook_Click(object sender, EventArgs e)
        {
            if (hLLHook == 0)
            {
                MouseHookLLProcedure = new HookProc(MouseHookForm.MouseLLHookProc);
                hLLHook = SetWindowsHookEx(WH_MOUSE_LL,
                            MouseHookLLProcedure,
                            GetModuleHandle(null),
                            0);
                if (hLLHook == 0){
                    MessageBox.Show("SetWindowsHookEx Failed");
                    return;
                }
                buttonMouseLowLevelHook.Text = "UnHook Windows Hook";
            }
            else
            {
                bool ret = UnhookWindowsHookEx(hLLHook);
                if (ret == false){
                    MessageBox.Show("UnhookWindowsHookEx Failed");
                    return;
                }
                hLLHook = 0;
                buttonMouseLowLevelHook.Text = "Set Windows Hook";
                this.Text = "Mouse Hook";
            }
        }
        private static int MouseHookProc(int nCode, IntPtr wParam, IntPtr lParam)
        {
            if (0 <= nCode)
            {
                MouseHookStruct MyMouseHookStruct = (MouseHookStruct)Marshal.PtrToStructure(lParam, typeof(MouseHookStruct));
                MouseHookForm tempForm = Form.ActiveForm as MouseHookForm;
                if (tempForm != null)
                {
                    String strCaption = "x = " + MyMouseHookStruct.pt.x.ToString("d") +
                                      "  y = " + MyMouseHookStruct.pt.y.ToString("d");
                    tempForm.labelStatus.Text = strCaption;
                }
            }
            return CallNextHookEx(hHook, nCode, wParam, lParam);
        }
        private static int MouseLLHookProc(int nCode, IntPtr wParam, IntPtr lParam)
        {
            if (nCode == HC_ACTION)
            {
                MouseLLHookStruct MyMouseHookStruct = (MouseLLHookStruct)Marshal.PtrToStructure(lParam, typeof(MouseLLHookStruct));
                MouseHookForm tempForm = Form.ActiveForm as MouseHookForm;
                if (tempForm != null && (WM_MOUSE_WHEEL == wParam.ToInt32()))
                {
                    String strCaption = "Wheeling x = " + MyMouseHookStruct.pt.x.ToString("d") +
                                      "  y = " + MyMouseHookStruct.pt.y.ToString("d");
                    tempForm.labelStatus.Text = strCaption;
                }
            }
            return CallNextHookEx(hHook, nCode, wParam, lParam);
        }
    }
}

べた書きでごめんなさい。張り付けて簡単に使って見れる構成にしたかったのです。。MSDNからパクッてきた左のボタンで有効になるのがローカルフックです。ボタンを押してフック有効にしてマウス移動させるとフォームの外に移動すると座標更新されないのがわかります。

WH_MOUSEによるフック

対して追加した右のボタンで有効になるのが今回のホイールをフックできるグローバルフックです。グローバルフック全体は.NET Frameworkでサポートされていないけどこのマウスホイール他いくつかはサポートされるよう(ただ動くだけかも)ですね。こちらはウィンドウをはみ出てもホイールしたタイミングで座標更新している様子が確認できます。

WH_MOUSE_LL

 2.親に頼む

別に何の面白みもなく、ごく当たり前なのですが、親コントロールに頼む方法があります。

subcontrol

namespace MouseWheelHook
{
    using System;
    using System.Windows.Forms;

    public partial class NeedWheelEventControl : UserControl
    {
        public NeedWheelEventControl()
        {
            InitializeComponent();
        }

        private void buttonAttachParent_Click(object sender, EventArgs e)
        {
            // 本来は適宜、静的or動的に親をさかのぼって探してください
            MouseHookForm parent = this.Parent as MouseHookForm;
            if (null == parent){
                return;
            }
            MouseEventHandler handler = (s, ea) => {
                labelStatus.Text = "Wheeling[2] x = " + ea.X.ToString("d") +
                                             "  y = " + ea.Y.ToString("d");

            };
            parent.MouseWheel += handler;
        }
    }
}

これを先ほどの親Formに張り付けて、コントロール外でマウスをホイールさせても当然イベントは取れます。
way2

親を探す方法をもうちょっと頑張って汎用的にすればライブラリ化もできそうですね。

Ctrl+Wheelで拡大縮小するWPFImageコントロール

今日は小ネタ。Ctrl+マウスホイールで拡大縮小するよくあるUIです。

画像を表示するのでImageコントロールの派生で作ってみます。

ZoomImage.cs

namespace ZoomableImage
{
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Media;
    using System.Windows.Input;

    public class ZoomImage : Image
    {
        private TransformGroup _transformGroup = new TransformGroup();
        protected override void OnMouseWheel(MouseWheelEventArgs e)
        {
            if ((Keyboard.Modifiers & ModifierKeys.Control) != ModifierKeys.None)
            {
                ChangeScale(e.GetPosition(this), e.Delta);
            }
            base.OnMouseWheel(e);
        }
        private void ChangeScale(Point center, int delta)
        {
            double scale = (0 < delta) ? 1.1 : (1.0 / 1.1);
            _transformGroup.Children.Add(new ScaleTransform(scale, scale, center.X, center.Y));
            RenderTransform = _transformGroup;
        }
    }
}

XAMLだとTransformGroupとか意識しませんがコードで書くとこのようになります。1つ1つの処理をGroup化できるので楽ですね、おまけに自然とコマンドパターンになっているのでUndo/Redoが簡単に実現できます。これをWindowに張り付けて、適当な画像をセットすればとりあえず動作開始。

MainWindow.xaml

<Window x:Class="ZoomableImage.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:my="clr-namespace:ZoomableImage"
        Title="MainWindow" Height="260" Width="350">
    <Grid>
        <my:ZoomImage Width="200" Height="100" Source="testimage.png"/>
    </Grid>
</Window>

起動状態は通常サイズの画像が表示されており、

first
これがCtrl+マウスホイールすると・・・・
zoomed
こんな感じに。

今回はGridの上で簡単に動くモノとしてはこれで良いかと思いますが、原点位置を精緻にコントロールしたい場合や他のレイアウトコントロールに配置する場合には注意が必要です。基本的にはマウス位置を中心に拡大・縮小してくれることを期待したいですが、等倍状態からの拡大縮小は正しく動作しますが拡大or縮小状態で中心がずれた位置でさらなる拡大or縮小をすると今回のソースではダメです。

また他のレイアウトコントロールに配置する場合ですが、例えばStackPanelに配置する場合を考えてみるとこのまま後ろにボタンを追加してしまうとズーム動作が変になります。

onstackpanel

これはTransformGroupを設定しているのがRenderTransformだからで、描画のためだけの変換処理なのです。実際にコントロールの配置を動的にレイアウトしなおしたい場合はLayoutTransformに設定する必要があります。そうすれば以下のようになります。

layouttransform

トライアンドエラーで習熟が必要ですね。

IValueConverterとTypeConverterの違い

WPFでバインディングをしていると登場する、似て非なる2つのConverterのおはなし。

それらはIValueConverterとTypeConverterです。私的な印象を言うと、IValueConverterはよく使うけどTypeConverterはあんまり使わない、IValueConverterは双方向の変換を用意することもあるがTypeConverterなら一方向だけの変換しか使わない、そんな感じ。

まずは今回のコンバーターで使うための簡単なクラスを定義しておきます。

ConversionNeeded.cs(1)

namespace WpfConvertersDifference
{
    using System;
    using System.ComponentModel;

    public class ConversionNeeded
    {
        public int inInt { get; set; }
        public string inString { get; set; }
        public double inDouble { get; set; }

        public override string ToString()
        {
            return inString + inInt + inDouble;
        }
    }
}

 

1.IValueConverterの作成

IValueConverterはバインディングをするときに値の変換を行うためのものです。前回・前々回で説明したとおり、データバインディングでは値をバインドのUIに渡しますが、その元の値がバインド先の型と違う時に変換するために用います。WPFのバインディングはバインド先は依存関係プロパティである必要があるため、IValueConverterは何かの型から依存関係プロパティの型に変換するものとなります。バインド先の型がstring型であると仮定して上のテスト用クラスからstringへの変換をするIValueConverterを実装するクラスを作ります。

InheritIValueConverter.cs

namespace WpfConvertersDifference
{
    using System;
    using System.Windows.Data;

    [ValueConversion(typeof(ConversionNeeded), typeof(string))]
    class InheritIValueConverter : IValueConverter
    {
        object IValueConverter.Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            ConversionNeeded src = (ConversionNeeded)value;
            return src.ToString();
        }

        object IValueConverter.ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

本来は必要に応じて逆変換も用意してください。

 

2.TypeConverterの作成

次はTypeConverterです。TypeConverterは.NET1.0からあるクラスでWPF(.NET3.0以降ですよね)の生まれる前から存在するクラスです。TypeConverterはXAMLパーサーが使うコンバーターです。というのもXAMLではほとんどすべてのプロパティや値をstringで指定しますが、それぞれが適切な型として適用されるにはその間にコンバーターが介在しているからで、それがTypeConverterなのです。数値や列挙型等組み込みのコンバーターは事前に定義されているので気付かぬうちに使っている、ありがたい存在です。なのでXAMLでTypeConverterを使う場合、変換元の多くはstring型です。ここではstring型から先ほどのテストクラスに変換する例をみてみます。
InheritTypeValueConverter.cs

namespace WpfConvertersDifference
{
    using System;
    using System.ComponentModel;
    public class InheritTypeConverter : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            if (sourceType == typeof(string))
            {
                return true;
            }
            return base.CanConvertFrom(context, sourceType);
        }
        public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
        {
            if (value is string)
            {
                if (((string)value).Contains("1")){
                    return new ConversionNeeded() { inDouble = 1.0, inInt = 1, inString = "one"};
                }
                return new ConversionNeeded();
            }
            return base.ConvertFrom(context, culture, value);
        }
    }
}

ConvertFromとCanConvertFromの2つをオーバーライドしました。逆変換もありますが、上記理由でオーバーライドはやめておきます。TypeConverterは単に実装するだけではだめでこれを適用するには変換対象のクラスに属性を付けておく必要があります。

ConversionNeeded.cs(2)

namespace WpfConvertersDifference
{
    using System;
    using System.ComponentModel;

    [TypeConverter(typeof(InheritTypeConverter))]
    public class ConversionNeeded
    {
        public int inInt { get; set; }
        public string inString { get; set; }
        public double inDouble { get; set; }

        public override string ToString()
        {
            return inString + inInt + inDouble;
        }
    }
}

 

3.2つのConverterを使う

さぁ2つのコンバーターを使うのですが、使用法がわかりやすくなるため上記ConversionNeededクラスをプロパティに持つクラスを定義しておきます。

 UserDefinedTestClass.cs

namespace WpfConvertersDifference
{
    public class UserDefinedTestClass
    {
        public ConversionNeeded InterProperty{ get; set; }
        public override string ToString()
        {
            return InterProperty.ToString();
        }
    }
}

ふたつのコンバーターを使ったMainWindowはこんな感じ。

<Window x:Class="WpfConvertersDifference.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:my="clr-namespace:WpfConvertersDifference"
        Title="MainWindow" Height="164" Width="276">
    <Window.Resources>
        <my:InheritIValueConverter x:Key="ivalConverter"/> <!-- ←IValueConverterをリソースに定義 -->
        <my:UserDefinedTestClass x:Key="obj" InterProperty="1"/> <!-- ←TypeConverterを使っている! -->
    </Window.Resources>
    <Grid>
        <TextBox HorizontalAlignment="Left" Height="25" Margin="56,50,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="154">
            <Binding Source="{StaticResource obj}" Path="InterProperty" Converter="{StaticResource ivalConverter}"/> <!--IValueConverterを使っている!-->
        </TextBox>
    </Grid>
</Window>

converter_動作様子

まずTypeConverterはXAML上の文字列からオブジェクトを作成するときに使われるのでした。UserDefinedTestClassのインスタンスを作成した際に、プロパティとしてバインド予定のオブジェクトを作成しようとしていますが、ここに”1″という文字列を渡しています。ここでInheritTypeConverterが作用して”1″という文字列から内部に1やoneといった値を持つConversionNeededオブジェクトを”変換“して作成します。もしInheritTypeConverterクラスが無いとここで”1″なんかで指定はできません。

次にIValueConverterはバインド時に値を変換するのでした。IValueConverterはBindingのConverterプロパティで指定するので、事前にリソースとして登録して利用できるようにしています。これにより、上記TypeConveterによって作成したUserDefinedTestClassのInterPropertyプロパティ(型はConversionNeeded)とTextBoxのText間の型違いを”変換“しています。

WPFでの単項データバインドまわり

前回はWindows Formsにおける単項データバインドでした。

WPFはそれ自体がBindingと深く関連づいておりネット上の情報も多いです。ただアレコレ登場するとわかりにくいかと思います、ここではWindowsFormsでやった単項データバインドをそのままWPFでライブラリを一切使わずやってみます。エッセンスだけを紹介しますのでライブラリを使ったら…,こんな時どうしたいか…,は他に譲りたいと思います。

 

データバインディング

WindowsFormsの場合同様、データバインドは【1.バインディング元になるデータ】,【2.バインディング先のUI】,【3.それらを接続するもの】の3つで構成されます。では【1.バインディング元になるデータ】は前回同様DisplayModelという単純クラスから始めていきます。

 DisplayModel.cs(1)

namespace WpfDataBindSample
{
    public class DisplayModel
    {
        private int a;
        private int b;

        public int A
        {
            get { return a; }
            set { a = value; }
        }
        public int B
        {
            get { return b; }
            set { b = value; }
        }
        public int Diff
        {
            get { return a - b; }
        }
    }
}

WindowsFormsの時と同じ単純なクラスから始まります。次は、【2.バインドする先のUI】を作ります。WPFなのでこんな感じ。

mainwindow

MainWindow.xaml(1)

<Window x:Class="WpfDataBindSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="187" Width="293" ResizeMode="NoResize">
    <Grid>
        <Label Content="A" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top"/>
        <Label Content="B" HorizontalAlignment="Left" Margin="10,53,0,0" VerticalAlignment="Top"/>
        <Label Content="Diff" HorizontalAlignment="Left" Margin="10,96,0,0" VerticalAlignment="Top"/>

        <Slider HorizontalAlignment="Left" Margin="77,16,0,0" VerticalAlignment="Top" Width="161"/>
        <TextBox HorizontalAlignment="Left" Height="23" Margin="77,53,0,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" Width="161"/>
        <Label Content="Label" HorizontalAlignment="Left" Margin="77,96,0,0" VerticalAlignment="Top" RenderTransformOrigin="-0.78,0.393"/>
    </Grid>
</Window>

ただコントロールを張り付けただけです。上から順にA, B, Diffをバインドするつもりで作りました。前回のWindowsFormsのエントリを読んでいただいた方はお気づきかもしれませんが、Aをバインドするコントロールは前回numericUpDownでした、それが今回はSliderになっています。その理由ですが、WPFにはnumericUpDownコントロールが存在しないためです。WPF上でもWindowsFormsのコントロールはWindowsFormsHostというWPFのコントロールを作ると使うことができますが、これからやろうとしているバインドができないのです!これをバインドさせる挑戦は今後やるとして、今回はSliderで代用しました。

最後に【3.それらを接続するもの】を作ります。WindowsFormsの場合BindingSourceでやりましたが、WPFではBindingクラスを使います。上のXAMLにBindingを施すとこんな感じ。

MainWindow.xaml(2)

<Window x:Class="WpfDataBindSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="187" Width="293" ResizeMode="NoResize">
    <Grid>
        <Label Content="A" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top"/>
        <Label Content="B" HorizontalAlignment="Left" Margin="10,53,0,0" VerticalAlignment="Top"/>
        <Label Content="Diff" HorizontalAlignment="Left" Margin="10,96,0,0" VerticalAlignment="Top"/>

        <Slider HorizontalAlignment="Left" Margin="77,16,0,0" VerticalAlignment="Top" Width="161">
            <Slider.Value>
                <Binding Path="A" UpdateSourceTrigger="PropertyChanged"/>
            </Slider.Value>
        </Slider>
        <TextBox HorizontalAlignment="Left" Height="23" Margin="77,53,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="161">
            <Binding Path="B" UpdateSourceTrigger="PropertyChanged"/>
        </TextBox>
        <Label Content="{Binding Path=Diff, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Margin="77,96,0,0" VerticalAlignment="Top" RenderTransformOrigin="-0.78,0.393" Height="28" Width="161"/>
    </Grid>
</Window>

いくつか書き方を変えてみましたが、全部同じです。Sliderに対してはSlider.Valueのプロパティ要素構文としてBindingを書いてみました。TextBoxに対してはTextBox.TextというプロパティにバインドしているのですがTextはコンテンツプロパティとして省略可能なのでこんな感じ。Labelに対してはマークアップ拡張という構文で書いてみました。上のSlider.Value, TextBox.Text, Label.Textはいずれも依存関係プロパティと呼ばれます。WPFのBindingはバインドできるのは”依存関係プロパティ”に限定されます、なのでWindowsFormsHost内のnumericUpDownはバインドできないのです。

ここではBindingを設定しましたが、UIは当然変わりません。Bindingを設定したら、実際にバインドするインスタンスを渡す必要があるので今回はDataContextを介してバインドします。

 MainWindow.xaml.cs

namespace WpfDataBindSample
{
    using System.Windows;
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new DisplayModel() { A = 5, B = 10 };
        }
    }
}

形だけはできました。これを実行してみるとわかりますが、UI→Modelへのバインドはできていますが、逆ができていません。前回やりましたね、INotifyPropertyChangedでModelからUIへ変更通知を送るんでした。

DisplayModel.cs(2)

namespace WpfDataBindSample
{
    using System.ComponentModel;
    public class DisplayModel : INotifyPropertyChanged
    {
        private int a;
        private int b;

        public int A
        {
            get { return a; }
            set {
                a = value;
                NotifyPropertyChanged("A");
                NotifyPropertyChanged("Diff");
            }
        }
        public int B
        {
            get { return b; }
            set {
                b = value;
                NotifyPropertyChanged("B");
                NotifyPropertyChanged("Diff");
            }
        }
        public int Diff
        {
            get { return a - b; }
        }

        public event PropertyChangedEventHandler PropertyChanged = (_, __) => {};
        private void NotifyPropertyChanged(string propertyName = "")
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

前回のWindowsFormsの場合ではDiffプロパティの変更通知を省略していました。これはバインド先のBindingSourceがINotifyPropertyChangedインターフェースとは無関係にデータを取得しなおすためでした。本来は今回のWPF版のように関連するプロパティ更新を通知する必要があります。これでバインドが動き出しました。

 

2.データ検証を追加する

WindowsFormsでのデータ検証はIDataErrorInfoを実装するんでした。WPFの場合も同じです!(上のINotifyPropertyChangedといい、同じことばかりですね)ただ、WindowsFormsの場合はerrorProviderコントロールがあったのですが、WPFにはありません。どうするのでしょうか?ま、実装していきましょう。

 DisplayModel.cs(3)

namespace WpfDataBindSample
{
    using System.ComponentModel;
    public class DisplayModel : INotifyPropertyChanged, IDataErrorInfo
    {
        private int a;
        private int b;

        public int A
        {
            get { return a; }
            set {
                a = value;
                NotifyPropertyChanged("A");
                NotifyPropertyChanged("Diff");
            }
        }
        public int B
        {
            get { return b; }
            set {
                b = value;
                NotifyPropertyChanged("B");
                NotifyPropertyChanged("Diff");
            }
        }
        public int Diff
        {
            get { return a - b; }
        }

        public event PropertyChangedEventHandler PropertyChanged = (_, __) => {};
        private void NotifyPropertyChanged(string propertyName = "")
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        public string Error{
            get { return string.Empty; }
        }
        public string this[string columnName]{
            get{
                if (columnName == "B" && B < 0){
                    return "負数は駄目だよ!";
                }
                return string.Empty;
            }
        }
    }
}

MainWindow.xaml(3)

<Window x:Class="WpfDataBindSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="187" Width="293" ResizeMode="NoResize">
    <Grid>
        <Label Content="A" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top"/>
        <Label Content="B" HorizontalAlignment="Left" Margin="10,53,0,0" VerticalAlignment="Top"/>
        <Label Content="Diff" HorizontalAlignment="Left" Margin="10,96,0,0" VerticalAlignment="Top"/>

        <Slider HorizontalAlignment="Left" Margin="77,16,0,0" VerticalAlignment="Top" Width="161">
            <Slider.Value>
                <Binding Path="A" UpdateSourceTrigger="PropertyChanged"/>
            </Slider.Value>
        </Slider>
        <TextBox HorizontalAlignment="Left" Height="23" Margin="77,53,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="161">
            <Binding Path="B" UpdateSourceTrigger="PropertyChanged" ValidatesOnDataErrors="True"/>
        </TextBox>
        <Label Content="{Binding Path=Diff, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Margin="77,96,0,0" VerticalAlignment="Top" RenderTransformOrigin="-0.78,0.393" Height="28" Width="161"/>
    </Grid>
</Window>

Xamlの(2)から(3)への変更点はTextBoxのBindingで、ValidatesOnDataErrosをTrueにしたことです。これをTrueにするとModelのIDataErrorInfoのエラーを受け付けられるようになるわけです。WindowsFormsでErrorProviderを付けた場合はこれがTrueになっているような状態と言えますね。これを実行するとこんな感じ。

idataerrorinfo_mainwindow

この状態でさらにWindowsFormsのErrorProviderのようにエラーメッセージを表示させるには、

 MainWindow.xaml(4)

<Window x:Class="WpfDataBindSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="187" Width="293" ResizeMode="NoResize">
    <Window.Resources>
        <Style TargetType="TextBox">
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="true">
                    <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>
    <Grid>
        <Label Content="A" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top"/>
        <Label Content="B" HorizontalAlignment="Left" Margin="10,53,0,0" VerticalAlignment="Top"/>
        <Label Content="Diff" HorizontalAlignment="Left" Margin="10,96,0,0" VerticalAlignment="Top"/>

        <Slider HorizontalAlignment="Left" Margin="77,16,0,0" VerticalAlignment="Top" Width="161">
            <Slider.Value>
                <Binding Path="A" UpdateSourceTrigger="PropertyChanged"/>
            </Slider.Value>
        </Slider>
        <TextBox HorizontalAlignment="Left" Height="23" Margin="77,53,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="161">
            <Binding Path="B" UpdateSourceTrigger="PropertyChanged" ValidatesOnDataErrors="True"/>
        </TextBox>
        <Label Content="{Binding Path=Diff, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Margin="77,96,0,0" VerticalAlignment="Top" RenderTransformOrigin="-0.78,0.393" Height="28" Width="161"/>
    </Grid>
</Window>

errorstring

エラーが1つもなかった状態から1つ発生すると、Validation.HasErrorがTrueになります。これをトリガーとしてTextBoxであればToolTipにエラーメッセージをバインドさせて表示させています。具体的なエラーはValidation.Errosコレクションに入ってくるので先頭要素を取り出しています。

さらに。WindowsFormsにもっと近づけようとエラー時にコントロールを赤枠で囲うんじゃなくて’‘アイコンを出したい場合は、Validation.ErrorTemplateを使いますがAdvancedな内容なのでここでは取り上げません。上記ではBindingのValidatesOnDataErrorsをTrueにすると、IDataErrorInfoのエラーが受け取れることがわかりました。WPFではバインド中に発生する例外も受け取ることができます(通常バインド中の例外は無視されます)。それにはValidatesOnExceptionsをTrueにするだけです。

さらにさらに。MSDN等を見ているとValidationRuleというクラスに気付くと思いますが、ValidatesOnDataErrorsをTrueにすることや、ValidatesOnExceptionsをTrueにすることはよく使うValidationRuleを適用しているということです。さらに凝ったことをしたい場合にValidationRuleを使うと覚えておけばよいかと思います。

WindowsFormsでの単項データバインドまわり

InteractiveExtensionsを自分で実装するシリーズは続きますが、より現場寄りなネタを1つ。

UIとModel間のデータバインドというとMFCのDDXからWPFのBindingまで長きに渡り続いていますが、最近データバインドでググるとWPFのネタは多くでますが、WindowsFormsとの境界が混じりわかりにくくなっている印象です。そこで本エントリではWPFは一切除外し、WindowsFormsでのデータバインドに絞ってご紹介したいと思います。

 

データバインドの方法

データバインドするには【1. バインドする元のデータ】、【2.バインドする先のUI】、【3.それらをつなぐもの】 の3つが不可欠です。概念図から。

bindingの構成1

まず1.バインドする元のデータはビジネスロジックに依るところですが、ここでは以下の例を元にします。

 DisplayModel.cs

namespace DataBindSample
{
    public class DisplayModel
    {
        private int a;
        private int b;

        public int A
        {
            set { a = value; }
            get { return a; }
        }
        public int B
        {
            set { b = value; }
            get { return b; }
        }
        public int Difference
        {
            get { return a - b; }
        }
    }
}

これで【1.バインドする元のデータ】が作成できました。次は、順に【2.バインドする先のUI】を以下のように作りましょう、ここではまだぺたぺたコントロールを張るだけです。
form
最後は【3.それらをつなぐもの】を作ります。これにはVisualStudioのウィザードを活用するのがオススメ。まず、VS2010ではツールメニュー内[データ] – [データソースの追加]ですが、VS2013では[表示] – [その他のウィンドウ] – [データソース]で選択してウィンドウを開き、[新しいデータソースの追加]を選択します。
datasourceの選択
オブジェクトを選択し[次へ]をクリック。
dataobjectの選択
先ほど作った【1.バインドする元のデータ】のクラスを選択します、これにてデータソースに今のクラスが追加されました。次に【2.バインドする先のUI】にBindingSourceコントロールをドラッグアンドドロップします。その上でDataSourceプロパティに先ほどのクラスを選択します。これにてBindingSourceを使う準備は一旦完了※ここではクラスを指定しただけなのでインスタンスは未割当なので後で割り当てます。

これをバインドするコントロールに順に割り当てていきます。バインド先のオブジェクトのDataBindingsの(詳細)ボタンをクリックし、バインドするプロパティを割り当てます。

setA

データ更新モードとはバインドするタイミングです。デフォルトでOnValidationとなっていますが、これではフォーカス遷移時にしかバインドが実行されません。今回のようなDiffという差分を常に確認したいアプリケーションではOnPropertyChangedのほうが適切かと思います。同様にB, DiffをそれぞれTextBox, Labelとバインドします。バインド先のnumericUpDownのValueはDecimalだけどTextBoxのTextはstringだけど、、大丈夫です。

最後にバインドするインスタンスを設定します。

namespace DataBindSample
{
    using System.Windows.Forms;
    public partial class BindingForm : Form
    {
        private DisplayModel _model;
        public BindingForm()
        {
            InitializeComponent();
            _model = new DisplayModel()
            {
                A = 50,
                B = 20
            };
            bindingSource.DataSource = _model;
        }
    }
}

これを実行するとバインドされA, B, Diffが常に整合のとれた状態になります。
動作の様子

 

双方向のデータバインド

あれ、INotifyPropertyChangedは?と思った方がいるのではないでしょうか。まず、INotifyPropertyChangedインターフェースとはデータの更新をUIに通達するためのインターフェースです。

bindingの構成2

INotifyPropertyChangedでは変更したプロパティ名をイベントを介してBindingSourceに通達します。ですが、先ほどの例ではINotifyPropertyChangedを実装していませんが、Diffが更新されていたのでしょうか?答えはINotifyPropertyChangedインターフェースの英語版のCommunityAdditionにあります。

inotifypropertychanged

INotifyPropertyChangedは変更のあったプロパティを渡せるのが通常の使い方ですが(全部のプロパティが更新された場合はプロパティ名にstring.emptyを指定します)、BindingSourceコントロールはINotifyPropertyChangedによらず全ての関連プロパティを取得しなおすのです。その分パフォーマンス低下は懸念されますが、先ほどの例ではINotiftyPropertyChangedの実装が不要になると言えます。

ではBindingSourceコントロールを使う限りINotifyPropertyChangedは実装が不要なのか?答えはNOです。先の例では次のコードを実行すると、不整合が生まれてしまいます。

reset

namespace DataBindSample
{
    using System.Windows.Forms;
    public partial class BindingForm : Form
    {
        private DisplayModel _model;
        public BindingForm()
        {
            InitializeComponent();
            _model = new DisplayModel()
            {
                A = 50,
                B = 20
            };
            bindingSource.DataSource = _model;
        }

        private void buttonReset_Click(object sender, System.EventArgs e)
        {
            _model.A = 100;
        }
    }
}

プログラムで値を書きかえられてもBindingSourceがその変更を知る術がないからです。
で、INotifyPropertyChangedを実装するとこんな感じ。

DisplayModel.cs

namespace DataBindSample
{
    using System.ComponentModel;
    public class DisplayModel : INotifyPropertyChanged
    {
        #region INotifyPropertyChangedメンバー
        public event PropertyChangedEventHandler PropertyChanged = (_, __) => { };
        #endregion

        private int a;
        private int b;

        public int A
        {
            set {
                a = value;
                NotifyPropertyChanged("A");
            }
            get { return a; }
        }
        public int B
        {
            set {
                b = value;
                NotifyPropertyChanged("B");
            }
            get { return b; }
        }
        public int Difference
        {
            get { return a - b; }
        }

        // C#5.0(≒VS2012)以降ではCallerMemberName属性が使える
        private void NotifyPropertyChanged(string propertyName = "")
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

本来ならA, Bの値セットで”Diff”プロパティも変ったよと教える必要がありますが、先ほどの「BindingSourceの各プロパティを一律取りなおす」という特性を利用してほったらかしにしています。(ォィ

 

単体検証を追加する

実際の商品レベルはまだまだ先です。数値をnumericUpDownにバインドした場合は良いのですが、textBoxにバインドしたところではユーザが自由に入力をできてしまいます。先ほどの例でやってみるとわかりますが、textBoxに ‘d’ などと数値以外の値を入れるとエラーも何も出ず、フォーカスが移動できなくなります。これは数値を入れないといけないのに対し、数値変換できない値を入れられバインドが不整合になるのを防いでいるのです。この状態ではそもそも何が問題かわかりません。これに対してはまずErrorProviderを使います。

useerrorprovider

Formに張り付けて、DataSourceプロパティに先ほどのbindingSourceを渡すだけ!これで先ほどの異常入力に対して何が問題かわかりますね。

error

さらに。Bの値は数値としてはOkだけど「 特定の値は禁止したい」場合などはIDataErrorInfoを使います。

namespace DataBindSample
{
    using System.ComponentModel;
    public class DisplayModel : INotifyPropertyChanged, IDataErrorInfo
    {
        #region INotifyPropertyChangedメンバー
        public event PropertyChangedEventHandler PropertyChanged = (_, __) => { };
        #endregion

        #region IDataErrorInfoメンバー
        public string Error
        {
            get { return string.Empty; }
        }
        public string this[string columnName]
        {
            get{
                if (columnName == "B" && B < 0){
                    return "負数は駄目だよ";
                }
                return string.Empty;
            }
        }
        #endregion

        private int a;
        private int b;

        public int A
        {
            set {
                a = value;
                NotifyPropertyChanged("A");
            }
            get { return a; }
        }
        public int B
        {
            set {
                b = value;
                NotifyPropertyChanged("B");
            }
            get { return b; }
        }
        public int Difference
        {
            get { return a - b; }
        }

        // C#5.0(≒VS2012)以降ではCallerMemberName属性が使える
        private void NotifyPropertyChanged(string propertyName = "")
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

これだけで先ほどのErrorProviderに自動的にエラーが追加されるようになります。

error2

この場合数値としてはOkなのでフォーカス移動は可能です(先ほどのdの場合はフォーカス移動もダメ)。この辺り凝ったつくりにしようとすると色々知っていないとハマッてしまうためオレオレ実装ができてしまうんではないかと思います。

Interactive Extensionsを自分で実装する – 11.IsEmpty

Interactive Extensions(Ix)を自分で実装する、11回目はIsEmptyです。

その名から予想できる通り、シーケンスが空かどうかを判断するためのものです。

static void IsEmptyTest()
{
    IEnumerable<int> noSequence = Enumerable.Range(1, 0); // count = 0
    var noSequenceResult = noSequence.IsEmpty();
    Console.WriteLine(noSequenceResult);

    IEnumerable<int> oneSequence = Enumerable.Range(1, 1);  // count = 1
    var oneSequnenceResult = oneSequence.IsEmpty();
    Console.WriteLine(oneSequnenceResult);
}

isempty
列挙アイテムが1つもなければTrue, 1つでもあればFalseを返します。実行例は省略しますが、nullに対してはちゃんとArgumentNullExceptionがスローされました。
では実装です。(Let’s Implement It!)

IsEmpty…シーケンスに列挙アイテムが1つでもあるかを確認する。

namespace EmulateInteractiveExtensions
{
    public static class EmulateIxExtensions
    {
        public static bool IsEmpty<TSource> (this IEnumerable<TSource> source)
        {
            if (source == null)
            {
                throw new ArgumentNullException();
            }
            return !source.GetEnumerator().MoveNext();
        }
    }
}

実装方法はいろいろあるかと思いますが、1文で済むのでIEnumeratorを使ってアイテムがあるかないかを判断しています。