銀の弾丸

プログラミングに関して、いろいろ書き残していければと思っております。

まだ間に合うXAMLの基本

XAMLはなんだか複雑だ」と思ってました。

しかし、あることに気が付いてから「なかなかシンプルなんじゃない?」と思えるようになりました。

ちゃんと知っている方にとっては、当たり前のことかと思いますが、その「ちょっとしたこと」を出発点に、1段掘り下げて調べた結果を書いておきます。

f:id:takamints:20161026121034p:plain
photo credit: Doug Kline Star Wars Celebration IV - X-Wing fighter (back) via photopin (license)

目次

  1. XAML要素でインスタンスが生成される
  2. 属性でパブリックプロパティを設定する
  3. プロパティへクラスオブジェクトを代入する
  4. 静的クラスのプロパティへオブジェクトを代入する
  5. そもそも子要素は何処に格納されるのか

1. XAML要素でインスタンスが生成される

XAMLの要素名はクラス名で、実行時には、そのクラスのデフォルトコンストラクタでインスタンスが生成される

クラス名であることは知っていましたが、デフォルトコンストラクタで生成されるという認識がありませんでした。 だから実行時のオブジェクトの状態を正確に把握できていなかったんですわ。

MainWindow.xamlMVVM的に真っ当にMessageBoxを表示する - 銀の弾丸より)

<Window x:Class="StudyDotNet.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:StudyDotNet"
        xmlns:vm="clr-namespace:StudyDotNet.ViewModels"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
        xmlns:tr="clr-namespace:StudyDotNet.Triggers"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>

    <i:Interaction.Triggers>
        <tr:MessageBoxTrigger>
            <tr:MessageBoxAction />
        </tr:MessageBoxTrigger>
    </i:Interaction.Triggers>

    <Grid>
        <Button x:Name="button" Content="Button"
                Command="{Binding SampleCommand}"
                HorizontalAlignment="Left" Margin="50,50,0,0"
                VerticalAlignment="Top" Width="75"/>
    </Grid>
</Window>
takamints.hatenablog.jp

上のXAMLでは、次のクラスオブジェクトが生成されています。 (ドットを含む要素名と、子要素の所在については後述します)

  • MainWindowViewModel
  • MessageBoxTrigger
  • MessageBoxAction
  • Grid
  • Button

確かにビューモデルやボタンなどは、実行時に生成されていて、コードビハインドからアクセスできますよね。 でも、デフォルトコンストラクタとの関連は想像していなかったんです。 このような、標準の部品に関しては、.NETフレームワーク内で特別なことが行われているのだろう・・・などと思ってました。

でも、自作クラスも生成されてるってことは・・・という段になって、やっと気付いた。

思った以上にシンプルですね。そもそもフレームワークをそんなガチガチに作るわけ無いですし。

2. 属性でパブリックプロパティを設定する

要素の属性はそのクラスオブジェクトのパブリックプロパティへの代入です。 デフォルトコンストラクタで生成されたのち、プロパティが設定されます。

属性値として直接記述できるのは、文字列とか数値といったプリミティブな型に制限されると思います。 バインディングの解決は、少し複雑なことがなされているのかもしれませんね。

3. プロパティへクラスオブジェクトを代入する

ところで、XAMLでデータコンテキストを生成しているところ(以下)。

<Window x:Class="StudyDotNet.MainWindow" ~中略~ >
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    ・
    ・
    ・
</Window>

この Window.DataContext要素のように、親エレメント.プロパティ名という要素は、親要素のインスタンスプロパティへの代入なんですね。

つまりここでは、MainWindowクラスの基本クラスであるWindowDataContextプロパティへ、MainWindowViewModelインスタンスを設定(Property Set)しているということです。

4. 静的クラスのプロパティへオブジェクトを代入する

以下のInteraction.Triggersも似ていますが、ドットの前がInteractionとなっていて、親要素ではありません。 これは、System.Windows.Interactivity.Interactionというスタティッククラスで、そのTriggersというプロパティにMessageBoxTriggerを追加しています。

<Window x:Class="StudyDotNet.MainWindow" ~中略~ >
    ・
    ・
    ・
    <i:Interaction.Triggers>
        <tr:MessageBoxTrigger>
            <tr:MessageBoxAction />
        </tr:MessageBoxTrigger>
    </i:Interaction.Triggers>
    ・
    ・
    ・
</Window>

ところが InteractionクラスにそのものスバリのTriggersというプロパティはありません。 かわりにTriggerCollection GetTriggers(DependencyObject obj)というメソッドがありますので、XAMLのパーサーがうまくやってくれているのだと思います(詳細不明)。

以下のように、コードビハインドからthisを与えて返されるコレクションに、ちゃんとMessageBoxTriggerインスタンスが入っていました。

using System;
using System.Windows;
using System.Windows.Interactivity;

namespace StudyDotNet
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            var triggers = Interaction.GetTriggers(this);
            foreach(var trigger in triggers)
            {
                Console.WriteLine(trigger.GetType().Name);
            }
        }
    }
}

ちなみにInitializeComponentの前では何も表示されなかったので、XAMLInitializeComponentで処理されているのでしょう。

5. そもそも子要素は何処に格納されるのか

XAMLのパーサーは、子要素のインスタンスを親要素の何処に格納するの?という疑問。 コレクションであることは間違いなさそうなのですが。

XAMLの以下の部分などですね。

<i:Interaction.Triggers>
    <tr:MessageBoxTrigger>
        <tr:MessageBoxAction />
    </tr:MessageBoxTrigger>
</i:Interaction.Triggers>

XAMLのパーサーが、MessageBoxTriggerのインスタンスに、MessageBoxActionのインスタンスを保持させている」ということは明らか。

実際にアクションはイベントトリガーのActionsというコレクションプロパティに保持されており、 このプロパティは、MessageBoxTriggerの基本クラスであるTriggerBaseに実装されています。 でも、XAMLだけを見る限りは、そんな事実はわからない。

ということで、System.Windows.Interactivity.TriggerBaseの説明を見てみると、以下のようになっています。

[ContentPropertyAttribute("Actions")] 
public abstract class TriggerBase : DependencyObject, IAttachedObject

なるほど、ContentPropertyAttributeで「XMLのコンテントはプロパティ名 Actions に」と読めますね。 XAMLのパーサーはこの宣言に従って子要素を親要素に格納している。

ということで、Interaction.Triggersの中身を、以下のように冗長に書き換えても同じように動作します。

<i:Interaction.Triggers>
    <tr:MessageBoxTrigger>
        <tr:MessageBoxTrigger.Actions>
            <tr:MessageBoxAction />
        </tr:MessageBoxTrigger.Actions>
    </tr:MessageBoxTrigger>
</i:Interaction.Triggers>

まとめと所感

このように、曖昧に済ませていたことを深掘りしてみて、かなりスッキリ気分爽快。 XAMLで何でもできる気がしてきました。

基本を押さえるのはホントに大切。 知識やスキルを習得するときの効率性に直結しますね。

いくつになってもお勉強です。


MVVM的に真っ当にMessageBoxを表示する

f:id:takamints:20161023233634p:plain
photo credit: waterj2 DSCF0629 via photopin (license)

MVVM的に正しくモーダルなメッセージボックスを表示するサンプルコードを示します。

WPFでのMVVMパターンとしては、ビュー以外からメッセージボックスを直接表示するのは良くないらしいです。

メッセージボックスで親ウィンドウを指定しないとモーダルにならないのですが「ビュー以外からメインウィンドウを逆方向へ参照するのは悪手」ということです。

じゃあ、どうすればよいかっていうと「メッセンジャーパターン」を使いましょうとのことでした。

メッセンジャーパターン」て?

メッセンジャーパターンは「VMからViewを操作する方法」のことらしいです。 以下のようなカラクリだと理解しました。

  1. メッセンジャー」というオブジェクトからイベントを発行して、
  2. あらかじめビューに実装されたイベントトリガーが、このイベントを受けて、
  3. イベントトリガーが保有するトリガーアクションが処理をする(イベントの引数も渡される)。

上に書いた点を自分なりに整理して、ややこしいことを抜きで System.Windows.MessageBox と同じように使えるコードをご紹介します。 GitHub Gistにも置いていますのでご自由にご利用ください。

コードの概要

  1. メッセンジャーとイベントトリガー(これが本体)
  2. トリガーアクションの実装例
  3. メインウィンドウ(MainWindow.xaml)の記述例
  4. メッセージボックスを表示する

ここで、

  1. メッセンジャーは単なるクラス。
  2. イベントトリガーは、System.Windows.Interactibity.EventTriggerの派生クラス。
  3. トリガーアクションは、System.Windows.Interactibity.TriggerActionの派生クラス。

※ System.Windows.Interactibityは、プロジェクトから参照されていないかもしれません。 参照マネージャの左のツリーから[アセンブリ]/[拡張]を開いてチェックを付ければOKです。

1.メッセンジャーとイベントトリガー(これが本体)

内部クラスを含めて3つのクラスを定義しています。System.Windows.MessageBoxと同じように使えるようにしたら長くなりました。

  1. MessageBox - メッセンジャー。スタティックなShowメソッドでシングルトンのインスタンスからイベントを発行。
  2. MessageBox.Action トリガーアクションの抽象基底クラス。実際にメッセージを表示するクラスの基本クラスです。
  3. MessageBoxTrigger - イベントトリガークラス。MessageBoxからのイベントを受け取って、トリガーアクションを実行します。

MessageBoxTrigger.cs

using System;
using System.Linq;
using System.Windows;
using System.Windows.Interactivity;

namespace StudyDotNet.Triggers
{
    /// <summary>
    /// MVVM的メッセージボックスを表示するためのメッセンジャー。
    ///
    /// MessageBox.Showメソッドで、イベントトリガーを起動する。
    /// 
    /// 実際の表示は、イベントトリガーから実行される
    /// トリガーアクションに実装される。
    /// </summary>
    public class MessageBox
    {
        /// <summary>
        /// MessageBoxTriggerを起動するイベント。
        /// </summary>
        public event EventHandler<EventArgs> ShowMessageBox;

        /// <summary>
        /// MessageBoxTriggerを起動するイベントの名前。
        /// </summary>
        public static string EventName
        { get { return "ShowMessageBox"; } }

        /// <summary>
        /// このクラスはシングルトン。
        /// </summary>
        public static MessageBox Instance
        { get; private set; } = new MessageBox();
        private MessageBox() { }

        /// <summary>
        /// MessageBoxTriggerを起動するイベントの引数。
        /// トリガーアクションへ渡されて処理される。
        /// </summary>
        public class EventArgs : System.EventArgs
        {
            public string Text { get; set; }
            public string Title { get; set; }
            public MessageBoxButton Button { get; set; }
            public MessageBoxImage Icon { get; set; }

            /// <summary>
            /// メッセージボックスの結果を受け取るコールバック
            /// </summary>
            public Action<MessageBoxResult> NotifyResult
            { get; set; }
        }

        /// <summary>
        /// MVVM的メッセージボックスを表示。
        /// 実際にはイベントを発行してイベントトリガーを起動する。
        /// </summary>
        /// <param name="messageBoxText"></param>
        /// <param name="title"></param>
        /// <param name="button"></param>
        /// <param name="icon"></param>
        /// <returns></returns>
        public static MessageBoxResult Show(
            string messageBoxText,
            string title = null,
            MessageBoxButton button = MessageBoxButton.OK,
            MessageBoxImage icon = MessageBoxImage.Information)
        {
            //メッセージボックスの結果
            MessageBoxResult messageBoxResult = MessageBoxResult.Cancel;

            //イベントを発行する
            Instance.ShowMessageBox?.Invoke(
                Instance,
                new MessageBox.EventArgs()
                {
                    Text = messageBoxText,
                    Title = title,
                    Button = button,
                    Icon = icon,

                    //コールバックで結果を受け取る
                    NotifyResult = result =>
                    {
                        messageBoxResult = result;
                    }
                });

            //メッセージボックスの結果を返す
            return messageBoxResult;
        }

        /// <summary>
        /// メッセージを表示するトリガーアクション実装用の抽象基底クラス。
        /// 
        /// 派生クラスでShowMessageを実装する。 
        /// </summary>
        public abstract class Action : TriggerAction<DependencyObject>
        {
            /// <summary>
            /// アクションの実態
            /// </summary>
            /// <param name="parameter"></param>
            protected override void Invoke(object parameter)
            {
                //イベント引数の種別を検査
                var messageBoxArgs = parameter as MessageBox.EventArgs;
                if(messageBoxArgs == null)
                {
                    return;
                }

                //メッセージボックスの表示結果を取得
                MessageBoxResult result = ShowMessage(
                        messageBoxArgs.Text,
                        messageBoxArgs.Title,
                        messageBoxArgs.Button,
                        messageBoxArgs.Icon);

                //コールバックで結果を通知
                messageBoxArgs.NotifyResult?.Invoke(result);
            }

            /// <summary>
            /// メッセージボックスを表示する抽象メソッド。
            /// </summary>
            /// <param name="text"></param>
            /// <param name="title"></param>
            /// <param name="button"></param>
            /// <param name="icon"></param>
            /// <returns></returns>
            abstract protected MessageBoxResult
            ShowMessage(
                string text, string title,
                MessageBoxButton button,
                MessageBoxImage icon);
        }
    }

    /// <summary>
    /// MVVM的メッセージボックスを表示するためのイベントトリガー。
    ///
    /// MessageBox メッセンジャーの発行するイベントで起動される。
    /// </summary>
    public class MessageBoxTrigger : System.Windows.Interactivity.EventTrigger
    {
        public MessageBoxTrigger()
            : base(MessageBox.EventName)
        {
            SourceObject = MessageBox.Instance;
        }
    }
}

2.トリガーアクションの実装例

MesssageBoxのトリガーから実行されるトリガーアクションクラスの実装例です。

MessageBox.Actionから派生した、MessageBoxActionクラスを実装しています。 メッセージボックスを表示して、その結果を返すクラスです。

このクラスから使っている MessageBox クラスは、System.Windows のものということに注意してください。

MessageBoxAction.cs

using System.Windows;

namespace StudyDotNet.Triggers
{
    /// <summary>
    /// メッセージボックスを表示するトリガーアクション
    /// </summary>
    public class MessageBoxAction : MessageBox.Action
    {
        protected override MessageBoxResult ShowMessage(
            string text,
            string title,
            MessageBoxButton button,
            MessageBoxImage icon)
        {
            var owner = Application.Current.Windows
                .OfType<Window>().SingleOrDefault(
                    x => x.IsActive);
            if(owner == null)
            {
                owner = Window.GetWindow(AssociatedObject);
            }
            return System.Windows.MessageBox.Show(
                owner, text,
                (title != null ? title :
                    Application.Current.MainWindow.Title),
                button, icon);
        }
    }
}

追記(2017-03-29): メッセージボックスのオーナーウィンドウに、アクティブウィンドウを指定するようにしました。メッセージボックスが出ている場合はnullになりうるのでその場合は従来通りのウィンドウとしています。(c# - Refer to active Window in WPF? - Stack Overflow

3.メインウィンドウ(MainWindow.xaml)の記述例

上のメッセージトリガーとトリガーアクションクラスは以下のように、Xamlに Interaction.Trigger 要素を追加します。

XML名前空間xmlns:ixmlns:tr の宣言にも注意。iは、System.Windows.Interactibityを使用するためで、 trは、上記のトリガー/アクションを使用するためのものです。

<Window x:Class="StudyDotNet.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:StudyDotNet"
        xmlns:vm="clr-namespace:StudyDotNet.ViewModels"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
        xmlns:tr="clr-namespace:StudyDotNet.Triggers"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>

    <!-- ここから -->
    <i:Interaction.Triggers>
        <tr:MessageBoxTrigger>
            <tr:MessageBoxAction />
        </tr:MessageBoxTrigger>
    </i:Interaction.Triggers>
    <!-- ここまで -->

    <Grid>
        <Button x:Name="button" Content="Button"
                Command="{Binding SampleCommand}"
                HorizontalAlignment="Left" Margin="50,50,0,0"
                VerticalAlignment="Top" Width="75"/>
    </Grid>
</Window>

4.メッセージボックスを表示する

使う側からは単純に、MessageBox.Showを呼び出すだけです。 System.Windows.MessageBoxとゴッチャにならないように注意は必要。

using System;
using System.Windows.Input;
using StudyDotNet.Triggers;

namespace StudyDotNet.Commands
{
    public class SampleCommand : ICommand
    {
        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter)
        { return true; }

        public void Execute(object parameter)
        {
            MessageBox.Show("MVVMバンザイ!");
        }
    }
}

いくつになってもお勉強

これによって、より少ない変更で、従来のメッセージボックス使用部分をメッセンジャーパターンに移行できます。

ユニットテストは、固定の応答を返すスタブで対応。

また、別の表示方法に切り替えるのも容易です。

例えば独自のウィンドウで表示したり、 ポップアップせずメインウィンドウ内にメッセージを配置したり、 履歴を参照する機能を追加したりと、広がりがあります。

ということで、やはりビュー以外からは、表示内容やUIに直接の関わりを持たないほうが良いのでしょう。

いくつになってもお勉強です。

WPFのMVVMでコマンドをバインディングする利点

f:id:takamints:20160924175447p:plain
photo credit: Storm Trooper at Oxford via photopin (license)

MVVMパターンでは、ボタンを押した時の処理などは、コマンドにバインディングしましょうということらしく、 従来の「Clickイベントをコードビハインドで受けて、、、」というのは嫌われるらしい。

しかし理由なく「MVVMでなきゃダメ!」と言われても納得しがたい。 「なぜ?」にはちゃんと答えて欲しいですよね。

ということで、この際きちんと理解しようと頑張りました。

いくつになってもお勉強です。

WHY JAPANESE PEOPLE!? [DVD]
WHY JAPANESE PEOPLE!? [DVD]
posted with amazlet at 16.09.24
アニプレックス (2015-06-24)
売り上げランキング: 10,600
C#実践開発手法 (マイクロソフト公式解説書)
Gary McLean Hall
日経BP
売り上げランキング: 152,501
目次

コマンドの実装例

具体的に、コマンドを利用するには

  1. 実行したい処理を、ICommandインターフェースを実装したコマンドクラスに記述して、
  2. ビューモデルから、そのインスタンスをプロパティで公開して、
  3. ボタンコントロールのCommand属性からバインディングする

と、これで幸せになれるらしい。

以降、以下の順に実装例を掲載してます。

  1. コマンドクラス:SampleCommand.cs
  2. ビューモデル MainWindowViewModel.cs
  3. ビュー:MainWindow.xaml
  4. ビューのコードビハインド:MainWindow.xaml.cs(おまけ)

コマンド:SampleCommand.cs

押してから5秒間、押せなくなるボタンです。 やってることは無意味ですが、 最低限ICommandインターフェースの全機能を使おうとして長くなってしまいました。

using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Input;

namespace StudyDotNet.Commands
{
    public class SampleCommand : ICommand
    {
        /// <summary>
        ///忙しいフラグ。忙しい時は何もできません。
        /// </summary>
        private bool _isBusy = false;
        
        /// <summary>
        /// 忙しいフラグのプロパティ。
        /// コマンドが実行可能かどうかに関連するプロパティなので。
        /// 代入されたらCanExecuteChangedイベントを投げる。
        /// </summary>
        public bool IsBusy
        {
            get { return _isBusy; }
            set
            {
                _isBusy = value;
                CanExecuteChanged?.Invoke(this, new EventArgs());
            }
        }

        /// <summary>
        /// 以下でタイマー使っているので、
        /// UIスレッドで画面更新するために必要
        /// </summary>
        private AsyncOperation _uiThreadOperation =
            AsyncOperationManager.CreateOperation(null);
        //
        // 以降 ICommand インターフェースの実装
        //

        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter)
        {
            //忙しくない時だけコマンド実行できる
            Console.WriteLine("実行可否を調べられてる。"
                + (!IsBusy?"お仕事できます":"今無理です"));
            return !IsBusy;
        }

        public void Execute(object parameter)
        {
            //忙しいフラグON
            //一定時間後には暇になる。
            Console.WriteLine("忙しくなるぞー");
            IsBusy = true;
            Timer _busyTimer = new Timer( timerParam => {
                _uiThreadOperation.Post(updatePropParam => {
                    IsBusy = false;
                    Console.WriteLine("暇になった。");
                }, null);
            }, null, 5000, Timeout.Infinite);
        }
    }
}
  • CanExecuteChangedイベント - コマンドを実行可否の状態変化時に発行するイベント。UIオブジェクトではこのイベントによって、画面表示状態を変更する。なので別スレッドから投げるときは、AsyncOperationでUIスレッドへActionをPostしなくてはなりませんね。
  • CanExecuteプロパティ - コマンドが実行可能な状況ではtrue、実行できない状況ならfalseを返すプロパティです。
  • Executeメソッド - コマンドの処理本体です。

ビューモデル MainWindowViewModel.cs

ビューモデルは非常にシンプル。上のコマンドクラスのインスタンスをパブリックプロパティとして持ってるだけ。 「この画面にはモデルを操作するための、こういう名前のコマンドがありますよ」と。そして「何をするかはコマンドを見て頂戴」というところでしょうか。

using StudyDotNet.Commands;

namespace StudyDotNet.ViewModels
{
    class MainWindowViewModel
    {
        public SampleCommand SampleCommand
        { get; private set; } = new SampleCommand();
    }
}

このプロパティのsetterからは、INotifyPropertyChangedインターフェースを実装して、PropertyChangedイベントを発行すべきだと思い込んでいましたが、なくても動いているようです(ここ、ちょっと理解が曖昧)。

ビュー:MainWindow.xaml

ビューからは ButtonのCommand属性からビューモデルのコマンドにバインディングButtonCommand="{Binding SampleCommand}"の部分ですね。

<Window x:Class="StudyDotNet.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:StudyDotNet"
        xmlns:vm="clr-namespace:StudyDotNet.ViewModels"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    <Grid>
        <Button x:Name="button" Content="Button"
                Command="{Binding SampleCommand}"
                HorizontalAlignment="Left" Margin="50,50,0,0"
                VerticalAlignment="Top" Width="75"/>
    </Grid>
</Window>

ビューのコードビハインド:MainWindow.xaml.cs(おまけ)

ちなみにMainWindowのコードビハインドは、何も触っていません。

using System.Windows;

namespace StudyDotNet
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

コマンドを使う利点

処理をコマンドクラスに記述することで得られる利点は、以下のようなものかと推測します。他にもたくさんありそな気もする。

  1. コードビハインド(*.xaml.cs)に何も書く必要がない。
  2. コマンドの単体テストで、ビューやビューモデルが不要です。

一言でいえば、コマンドの独立性が保たれて、ビューモデルがシンプルに保てます。疎結合はいつでも正義。

コマンドの実行可否がコマンド自体から得られます(CanExecuteプロパティ)し、UIオブジェクトに直結しているので、別途IsEnabledプロパティでバインディングしなくても良いのです。複数の条件が絡み合ったUIオブジェクトが複数ある場合、ビューモデルにIs〇〇ButtonEnabledみたいなプロパティがゴロゴロ沸いてきて煩雑になりやすいですからね。

コマンド処理のアンチパターン

上に書いたように、コマンドには明らかなメリットがありますが、一発で台無しにする方法があります。

それは、コマンドの処理内で昔ながらのメッセージボックスやダイアログを直接モーダル表示することです。

お手軽なので、ついついエラーの表示や問い合わせ等で使いがちですが、よろしくない。

なぜかというとユニットテストが自動ではなくなってしまいます。例えばユーザーがOKボタンを押さないとテストが進まないという事態になります。あとモーダルダイアログとして表示するためにはメインウィンドウが必要なのですが、コマンドの処理の中でメインウィンドウを参照するのもコードの独立性が破れて嫌われます。

コマンド処理内でユーザー入力が必要な場合は、MVVMでの「メッセンジャーパターン」という機構で回避できます。 ユニットテストでは簡単に応答を返すスタブに差し替えられます。 この「メッセンジャーパターン」については、以下のページに詳しく書いていますので是非。

takamints.hatenablog.jp

いくつになってもお勉強

設計思想的には古くからあるDocument-ViewやMVCなどを最新技術で発展させたもののようですね。

古くはVC++MFCアプリケーションでメニュー項目などを更新するUpdateUIなどと同じ便利さ加減と思います。便利なんだけどひと手間必要というのも同じですね。当時、あの機構をまともに理解している人が少なくて、往生した覚えがありますわって話が古くて伝わってない?

なにより、「どんな利点があるのか」という観点から、腰を据えて理解できて非常によかった。

フタを開ければかつて知ったる設計思想的な気分でもありました。

やっぱり、いくつになってもお勉強ですねー。


関連記事:

takamints.hatenablog.jp

C#のラムダ式はAction・Funcと一緒に理解する

f:id:takamints:20160922160154p:plain
photo credit: Imperial Shuttle via photopin (license)

C#ラムダ式をスッキリ理解する方法です。

何事も表面的な丸暗記でなく、基礎からキッチリ理解するほうが結果的には早道ですね。

C#ラムダ式に関しては、ActionクラスととFuncクラスをしっかり理解しておけば、それほど難しくはないはずです。

C#のActionクラスは、戻り値のないメソッドを表すクラスで、Funcは戻り値のあるメソッドです。

どちらもジェネリッククラスで、Actionは引数リスト、Funcは戻り値の型と引数リストをジェネリックパラメータで指定します。

Actionはメソッドの動作内容に注目した名前であり、Funcは数学的な関数として評価値を持つ(=戻り値がある)ということですね。

この2つのクラスについては以下のページで詳しく書いていますので参照してください。

takamints.hatenablog.jp

目次

C#ラムダ式

C#逆引きレシピ
C#逆引きレシピ
posted with amazlet at 16.09.22
arton
翔泳社
売り上げランキング: 97,981

ラムダ式の記法的には、以下のような感じ。引数がひとつなら丸かっこは不要とか、波かっこの中身が単一の文なら(複文でないなら)波かっこは要らないとか、いろいろあるんだけど、基本はこちらでOKです。

(name,age) => {
    Console.WriteLine(
        string.Format(
            "{0} is {1} years old",
            name, age));
};

型とか指定されていないし、戻り値ってありなの?無しなの?どうでもいいの?てな具合に、全く情緒が安定しない代物ですが・・・

ラムダ式は即時呼び出しできません

JavaScript的にラムダ式を直接呼び出そうとしてエラーになって「何がダメなの?」と混乱しました。 「C#ラムダ式ってJavaScriptの無名関数と一緒でしょ?関数オブジェクトそのものでしょ?」的な思い込みがあったんですね。

でもそれは間違い。C#ラムダ式は、それそのものを呼び出すことができません。

ラムダ式というランタイムオブジェクトは無い

C#ラムダ式は、ActionクラスかFuncクラスのインスタンスを生成するためのものなんですよね。

そもそもこれまで、ラムダ式を見たとき「型が明示されていないのに、どうコンパイルされて実行されているのだ?」と思っていたのですが、正しく動いているコードなら、一見欠落しているように見える引数や戻り値の型情報は、一緒に使われている ActionFunc、またはデリゲートで推測可能になってるはず。

ラムダ式はそれらのオブジェクトを生成するために使われるのですが、ランタイムに「ラムダ式」というオブジェクトとして存在しているわけではないということです。

単なる記法、シンタックス・シュガーです

つまり「ラムダ式は、デリゲートやActionやFuncを記述するためのシンタックス・シュガー」であって、それ自体はオブジェクトでもなんでもなく「単なる記法」というわけです。

その証拠に、JavaScriptの即時関数(以下)のようなコードは、C#ラムダ式では実装不可です。コンパイルが通りません。

//JavaScriptの即時関数呼び出し
var a = 1;
(function(b) {
    a += b;
}(2));
console.log("a:", a);// "a: 3"

C#では、一旦Actionインスタンスを作ってからでないと実行できません。

なぜかというと、ラムダ式だけでは、引数の型が特定できないからなんですね。

以下のコードでは、Actionクラスによって、第一引数がintであることが明示されている(あえて似せて書いていますので無駄に丸かっこが付いています)ので実行可能となるわけです。

int a = 1;
(new Action<int>(b => {
    a += b;
})(2));
Console.WriteLine(string.Format("a: {0}", a));

JavaScriptでの無名関数は関数オブジェクトですが、C#ラムダ式に直接対応するオブジェクトはないってことです。 ちなみにJavaラムダ式は無名クラスのインスタンスなんですね。 言語によって扱いが異なるのは興味深いがヤヤコシイ。

セルの範囲に入力された最も右の値を得るには「MATCH」を使う

f:id:takamints:20160831214321p:plain
photo credit: Bailey holding a mug via photopin (license)

セルの範囲内で、最も右の入力された(空白でない)値を得る には、ワークシート関数 MATCH が使えるんですね(INDEXとMAXも使います)。


かれこれ20年以上エクセルを使い続けていますが、今になって新たなワークシート関数を知って感心するとは思ってもいませんでした。 長年VBAのマクロでやっていましたが計算式だとお手軽です。 そして、このワークシート関数が使えるのはエクセルだけではありません。Google Driveスプレッドシートでも使えます。 以降で実際の Google スプレッドシートを共有していますので、ご利用ください。

目次

関連記事はこちら↓になります:
takamints.hatenablog.jp

事の発端は進捗管理

先日、個人作業の進捗管理のシートを軽い気持ちで作っていたんですね。

縦方向に作業項目を並べて、横方向には当日から締め切りまでの日付が並ぶ。各項目の進捗状況を日毎に記録するものです。

期限内に全作業がきちんと終了するよう自己管理。日々の状況を把握して、対策打ったり、あきらめたり(笑)、ふさぎ込んだり(!?)ってな使い方です。

客観的マネージメントには数値管理が必要ですし、なにより残工数とか表示しちゃって、予測曲線プロットしたりと、もうデータフェチにはたまらない喜びなわけですよ。理解できないかもしれないけど。

一番右が欲しいのです

作業項目は、1人日(いちにんにち = 一人の人が一日でできる作業)程度に分割しておき、各項目行の日毎のセルに0.5=「今やってます」とか、1.0=「完了した」とか書いていく。

でも、完了したり、そのあと進捗に変化がない日には、同じ数値を入れたくない。手間だし、表としても見にくいし。

なので、各項目の最新の進捗率は、各行の中で、数値が入力されているセルのうち、一番右にあるセルの値を取り出したい

過去にはVBAのマクロでやってたんですよ。 でも入力時のトリガーで再計算とか結構遅くなりますし、別のシートへ展開しにくいんですよね。 データ管理用のシートに分けたりしてみましたが、行や列の挿入で簡単に破たんしてしまいます。 この辺はホント「エクセルあるある」だと思います。

なので、できれば計算式でやりたいが、そのやり方が分からなかったのです。

過去の自分にググレカス

で、この度ググってみたら以下のサイトが見つかった。

EXCELで、範囲指定した一番右の数値(セル)を返す関数ってありますか?...-Yahoo!知恵袋
detail.chiebukuro.yahoo.co.jp

というか他にもゴロゴロ出てきますやん。しかもそれぞれ、結構古くて2009年とか、7年前の情報です。

なんで今まで検索しなかった?と不思議でしたが、よくよく思い出してみると、ちょうどワタシが管理業務から逃れた離れることになった時期。 当時、このような管理面のことを、(一時的に)ちっとも考えなくなっていたんだな。ダメですね。

MATCHで~す

マッチ箱の脳(AI)―使える人工知能のお話
森川幸人 (2014-01-05)
売り上げランキング: 1,343
モノワイヤレス TWE-001L-DIC-WA TWE-Lite DIPシリーズ 端子付き(マッチ棒アンテナ)
モノワイヤレス(Mono Wireless)
売り上げランキング: 26,668

まあ、とにかく、そこで紹介されているのは、以下のような計算式でした。

セル範囲A1:E1に入力されてる最も右の値を得る:

= INDEX ( A1:E1, MATCH( MAX(A1:E1) + 1, A1:E1, 1 ))

INDEX関数

INDEX関数は、セル範囲からインデックスを指定して値を得る関数です。 第一引数がセルの範囲で、第二引数がインデックス。上の式ではA1:E1がセル範囲。MATCH( ... )がインデックスですね。

MATCH関数

で、INDEXの第二引数に指定されてるMATCH関数は、一次元のセル範囲(つまり1行か1列)から検索値にマッチする値が入力されたセルの位置(インデックス)を返す関数です。第一引数が検索値、第二引数がセルの範囲、第三引数が検索の型。

MAX関数

その名の通り、範囲内から最大値を得る関数ですね。

ちょっと待って、なんで最大値を探すのだ?

しかしちょっと待ってくださいよ、上の例では、検索値が、MAX(A1:E1)+1となっていて、セル範囲内の最大値+1となっているではないですか。これ検索してもダメなんじゃ?!と思ったのですが、どうやらミソは第三引数の検索の型。検索の型が1だと、検索値以下の最大値にヒットするのだとか。(だから本来+1する必要はないのだと思います)。しかも省略時のデフォルトが1なので省略してもよいみたい。

まだワカラン。一番右が最大だとは限らんのでは?

しかしまだ、納得できない。釈然としない。最大値の位置を知っても、それが一番右にあるとは限らないのだから、これでは目的の値は得られないのでは?と疑いましたが、なんとMATCH関数の説明に、セル範囲内のデータは昇順にソートされている前提と書かれているんですね。

エクセルのヘルプでは、たまにこういった軽く意味不明の記述がありまして、見るたびに「なんちゅう都合のいい仕様?!www」と嘲笑とか憤慨を覚えていました。

しかし、どうやら、この前提のおかげで、最後まで検索してくれるよう。こういう目的で使いたいから、こんな妙な前提を入れているのかなとか思ったりもしたがはて?。

サンプル(Googleスプレッドシート

以下に、Googleスプレッドで作ったサンプルを埋め込んでいますが計算式が読めないのでこちらからどうぞ。