メモの日々


2017年08月16日(水) [長年日記]

[c#] WPFのバインディング処理で投げられた例外をDispatcherUnhandledExceptionイベントのハンドラで受け取る

WPFアプリケーションにおいて例外をApplication.DispatcherUnhandledExceptionイベントのハンドラで受け取ってエラーダイアログを表示するようにしてみたが、バインディングの処理において投げられた例外は受け取れないことを知った。

対処として、例外を投げうる処理をasync/awaitを使った非同期メソッドで実行するようにすればハンドラで例外を受け取れそうなのでサンプルをメモ。

MainWindow.xaml

<Window
        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:Sample"
        xmlns:local_e="clr-namespace:Sample.例外"
        x:Class="Sample.MainWindow"
        mc:Ignorable="d"
        Title="MainWindow" Height="100" Width="300">
    <Window.DataContext>
        <local_e:ExceptionViewModel/>
    </Window.DataContext>
    <Grid>
        <ComboBox Height="20" Width="200"
                  ItemsSource="{Binding Items}"
                  SelectedItem="{Binding Selected}"/>
    </Grid>
</Window>
  • ComboBoxで選択した内容がExceptionViewModelクラスのSelectedプロパティに設定されるようにしている。

ExceptionViewModel.cs

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace Sample.例外
{
    class ExceptionViewModel
    {
        private List<string> items = new List<string>() { "throw", "task", "async" };
        private string selected;

        public List<string> Items => items;

        public string Selected
        {
            get => selected;
            set
            {
                selected = value;
                switch (selected)
                {
                    case "throw":
                        ThrowException(selected);
                        break;
                    case "task":
                        Task.Run(() => ThrowException(selected));
                        break;
                    case "async":
                        Async(() => ThrowException(selected));
                        break;
                }
                Debug.WriteLine($"{selected}: end of property");
            }
        }

        private static void ThrowException(string message)
        {
            Thread.Sleep(3000);
            Debug.WriteLine($"{message}: throw exception");
            throw new Exception(message);
        }

        private static async void Async(Action action)
        {
            await Task.Run(action);
        }
    }
}
  • Selectedプロパティのセッターで例外を投げるThrowException()を呼び出している。
    • 設定値が「throw」の場合は単にThrowException()を呼び出している。ここで投げられた例外は後述のイベントハンドラまで届かない。
    • 設定値が「task」の場合は、Taskクラスを使って別スレッドでThrowException()を呼び出している。ここで投げられた例外も後述のイベントハンドラまで届かない。
    • 設定値が「async」の場合は、非同期メソッドであるAsync()の中でThrowException()を呼び出している。ここで投げられた例外は後述のイベントハンドラまで届く。

App.xaml

<Application x:Class="Sample.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:Sample"
             StartupUri="MainWindow.xaml"
             DispatcherUnhandledException="HandleException">
    <Application.Resources>
         
    </Application.Resources>
</Application>
  • App.xamlでApplication.DispatcherUnhandledExceptionイベントのハンドラを設定している。

App.xaml.cs

using System.Windows;
using System.Windows.Threading;

namespace Sample
{
    public partial class App : Application
    {
        public void HandleException(
            object sender, DispatcherUnhandledExceptionEventArgs e)
        {
            System.Diagnostics.Debug.WriteLine($"Handled: {e.Exception.Message}");
            e.Handled = true;
        }
    }
}
  • イベントハンドラではデバッグ出力をするだけ。

実行結果

ComboBoxでthrowを選択すると、デバッグ出力には

throw: throw exception
例外がスローされました: 'System.Exception' (Sample.exe の中)
System.Windows.Data Error: 8 : Cannot save value from target back to source. BindingExpression:Path=Selected; DataItem='ExceptionViewModel' (HashCode=66824994); target element is 'ComboBox' (Name=''); target property is 'SelectedItem' (type 'Object') Exception:'System.Exception: throw
   場所 System.ComponentModel.ReflectPropertyDescriptor.SetValue(Object component, Object value)
   場所 MS.Internal.Data.PropertyPathWorker.SetValue(Object item, Object value)
   場所 MS.Internal.Data.ClrBindingWorker.UpdateValue(Object value)
   場所 System.Windows.Data.BindingExpression.UpdateSource(Object value)'

と出力される。イベントハンドラには到達していない。バインディングの処理の中で例外がcatchされてしまっているからだろうか。

ComboBoxでtaskを選択すると、デバッグ出力には

task: end of property
task: throw exception
例外がスローされました: 'System.Exception' (Sample.exe の中)

と出力される。イベントハンドラには到達していない。例外はTaskオブジェクトが受け取ったけれども誰も参照しなかったと考えればいいだろうか。

ComboBoxでasyncを選択すると、デバッグ出力には

async: end of property
async: throw exception
例外がスローされました: 'System.Exception' (Sample.exe の中)
例外がスローされました: 'System.Exception' (mscorlib.dll の中)
Handled: async

と出力される。イベントハンドラに到達している。非同期メソッドは便利だと考えればいいだろうか。ただし、要件によっては当該処理を非同期で実行すると問題があるケースもありそう。重複実行を防ぐ必要があるとか。


2017年07月05日(水) [長年日記]

[windows] Excelでテキストボックスなどのオブジェクトを全選択する

たくさんのテキストボックスが設定されているExcelファイルからすべてのテキストボックスを削除したかった。次のようにやるのがよさそう。Excelのバージョンは2013。

  1. リボンで「ホーム」タブを選択。
  2. リボンの右の方にある「検索と選択」メニューを開く。
  3. メニューから「条件を選択してジャンプ」を選択。
  4. 選択オプションダイアログが開くので、左下にある「オブジェクト」を選択してOKボタンをクリック。

これでシート内のすべてのテキストボックスが選択状態になるので、この後Deleteキーを押せばすべてのテキストボックスを削除できる。ただし、シート内にテキストボックス以外の「オブジェクト」があるとそれも一緒に削除されてしまうことに注意。

それから、「検索と選択」メニューにある「オブジェクトの選択と表示」を選択するとウィンドウの右側に「選択」作業ウィンドウが現れシート内のオブジェクト一覧が表示される。これも便利なのでメモ。


2017年06月12日(月) [長年日記]

[c#] WPFでICommandとINotifyPropertyChangedを使う

WPFではボタンやメニューをクリックしたときにICommandを実装したクラス(コマンド)のメソッドを呼び出す仕組みがある。また、INotifyPropertyChangedを実装することでViewModelの状態変化をViewへ通知することができる。これらの機能を使った次のプログラムを作ってみた。

画面イメージ

  • 追加ボタンをクリックするとテキストボックスの内容を画面下部に追加する。このときテキストボックスの内容はクリアする。
  • テキストボックスが空だとボタンはクリックできない。

MainWindow.xaml

<Window x:Class="Hello.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:Hello"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525"
        FocusManager.FocusedElement="{Binding ElementName=textBox}">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <TextBox x:Name="textBox"
                 Height="18" Margin="5" VerticalAlignment="Top"
                 Text="{Binding InputText, UpdateSourceTrigger=PropertyChanged}"/>
        <Button x:Name="button" Content="追加" Grid.Row="1"
                Width="75" Height="20" Margin="5"
                HorizontalAlignment="Left" VerticalAlignment="Top"
                Command="{Binding AddCommand}"/>
        <ScrollViewer Grid.Row="2">
            <TextBlock x:Name="textBlock" Text="{Binding Message}"/>
        </ScrollViewer>
    </Grid>
</Window>
  • 前回はDataContextをXAML上で指定したけど、これだとモデルが複雑になったときにうまくいかない気がしてきたので指定しないようにした。DataContextは後述のApp.xaml.cs上で指定する。
  • 起動時にTextBoxにフォーカスを当てるよう、Window要素にFocusManager.FocusedElement属性を指定してみた。
  • TextBoxのテキストが変化する度にViewModelが更新されるよう、TextBoxのText属性にて UpdateSourceTrigger=PropertyChanged を指定した。
  • ButtonのCommand属性にAddCommandを指定した。これによりこのボタンとViewModelのAddCommandプロパティで参照できるコマンドが関連づく。
  • TextBlockをScrollViewerで囲い、TextBlockのテキストが長くなった時にスクロールできるようにした。

App.xaml

<Application x:Class="Hello.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:Hello"
             Startup="onStartup">
    <Application.Resources>
         
    </Application.Resources>
</Application>
  • VisualStudioの生成するApp.xamlはApplication要素のStartupUri属性でXAMLファイルを指定しているが、それを削除しStartup属性を指定するように変更した。これで起動時にHello.AppクラスのonStartup()が呼ばれるようになる。

App.xaml.cs

using System.Windows;

namespace Hello
{
    /// <summary>
    /// App.xaml の相互作用ロジック
    /// </summary>
    public partial class App : Application
    {
        private void onStartup(object sender, StartupEventArgs e)
        {
            var mh = new MessageHolder("こんにちは");
            var w = new MainWindow();
            w.DataContext = new MainWindowViewModel(mh);
            w.Show();
        }
    }
}
  • AppクラスにonStartup()を追加し、ここでMessageHolder(モデル)、MainWindowViewModel(ビューモデル)、MainWindow(ビュー)を生成して関連づけるようにした。

MessageHolder.cs

namespace Hello
{
    class MessageHolder
    {
        public MessageHolder(string initialMessage)
        {
            Message = initialMessage;
        }

        public string Message { get; set; }
    }
}
  • モデルクラスを作った。文字列をプロパティに持つだけ。

MainWindowViewModel.cs

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace Hello
{
    class MainWindowViewModel : INotifyPropertyChanged
    {
        private MessageHolder messageHolder;
        private ICommand addCommand;

        public MainWindowViewModel(MessageHolder mh)
        {
            messageHolder = mh;
            addCommand = new DelegateCommand(
                () =>
                {
                    Message = InputText + "\n" + Message;
                    InputText = "";
                    NotifyPropertyChanged(nameof(InputText));
                },
                () => { return !string.IsNullOrEmpty(InputText); });
        }

        public string Message
        {
            get { return messageHolder.Message; }
            set
            {
                messageHolder.Message = value;
                NotifyPropertyChanged();
            }
        }

        public string InputText { get; set; }

        public ICommand AddCommand => addCommand;

        public event PropertyChangedEventHandler PropertyChanged = delegate {};

        private void NotifyPropertyChanged([CallerMemberName] string name = "")
        {
            PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
    }
}
  • ViewModelはINotifyPropertyChangedを実装するようにした。最後にある NotifyPropertyChanged() を呼ぶと状態の変化がViewへ通知される。
  • NotifyPropertyChanged()の引数には[CallerMemberName]属性を指定した。こうすると呼び出し元のメソッドorプロパティ名が設定されるみたい。
  • フィールドaddCommandに後述するDelegateCommandのインスタンスを設定している。これをAddCommandプロパティで公開しViewがそれを参照している。

DelegateCommand.cs

using System;
using System.Windows.Input;

namespace Hello
{
    class DelegateCommand : ICommand
    {
        private Func<bool> canExecute;
        private Action execute;
        
        public DelegateCommand(Action execute) : this(execute, () => true) {}

        public DelegateCommand(Action execute, Func<bool> canExecute)
        {
            this.execute = execute;
            this.canExecute = canExecute;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

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

        public void Execute(object parameter)
        {
            execute();
        }
    }
}
  • コマンドの処理はViewModelのメソッドで処理するのが手軽なので、それを実現するためのコマンドDelegateCommandを定義する。MVVMフレームワークを使う場合はフレームワークが同様のクラスを用意しているみたい。
  • ICommandで受け取れるパラメータはobject型なので使いづらい。なのでパラメータは使わないようにしている。コマンドの状態は処理が移譲されるViewModelの状態から取得する想定。
  • ICommandはCanExecute()を持つが、これが有効に機能するにはこのメソッドの返す値が変わったときにコマンドの送信元(今回の場合はButton)に状態変化を通知する必要がある。で、上では状態通知のイベントハンドラをCommandManagerに登録しているが、こうすると自動的に状態変化通知がされるみたい。仕組みがよくわからないけれど適切なタイミングでポーリングしているのだろうか。
  • TODO CommandManager.RequerySuggestedはハンドラを弱参照で持つからハンドラがGCで回収されてしまわないように気をつけろという注意が書かれていた。修正しないと駄目そう。
    • 別に問題ない気がしてきた。DelegateCommandへイベントハンドラを登録するのはボタンなどのWPFのコンポーネントで、これらはイベントハンドラへの参照を保持しているはずだと思う。もし問題が発生したら調べる。

2017年06月09日(金) [長年日記]

[c#] WPFでHello World

VisualStudio 2015を使ってWPFアプリケーションを作ろうとしている。難しいのでメモしていきたい。

まずはHello World。出来上がりはこれだ。

画面イメージ

MainWindow.xaml

<Window x:Class="Hello.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:Hello"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <Grid>
        <TextBlock x:Name="textBlock" Text="{Binding Text}"/>
    </Grid>
</Window>
  • MVVMにしたいのでViewModelクラスを参照する。ViewとViewModelの結合をどう実現するのがいいのかまだよくわからないが、XAML上で指定するのが一番わかりやすい気がするので <Window.DataContext> を使ってViewModelクラスを指定した。
  • ウィンドウにはテキスト表示用にTextBlockを置いた。
    • TextBlockのTextプロパティにViewModelとのバインディングを記述した。「{Binding Text}」と書くと、DataContextに指定したクラスのインスタンスのTextプロパティの値が設定される。

MainWindowViewModel.cs

namespace Hello
{
    class MainWindowViewModel
    {
        public string Text => "Hello World";
    }
}
  • ViewModelクラスは今の所何も継承しなくていいはず。Textプロパティを定義した。

上記2つのファイルを編集・作成すればアプリケーションが完成する。


2017年05月31日(水) [長年日記]

[shell] Bashで配列

Bashでは

a=(a b c d e)

のように()を使うと配列を生成することができる。

要素の参照

echo ${a[0]}      #=> a
echo ${a[1]}      #=> b
echo ${a[-1]}     #=> e
echo ${a[@]}      #=> a b c d e
echo ${a[@]:2}    #=> c d e
echo ${a[@]:2:2}  #=> c d

要素数とインデックスの参照

echo ${#a[@]}     #=> 5
echo ${!a[@]}     #=> 0 1 2 3 4

要素の追加と削除

a+=(hello)   && echo ${a[@]}  #=> a b c d e hello
a[100]=world && echo ${a[@]}  #=> a b c d e hello world
unset a[2]   && echo ${a[@]}  #=> a b d e hello world
echo ${!a[@]}                 #=> 0 1 3 4 5 100

[shell] Bashで計算

Bashは算術式展開を使うと簡単な計算ができる。

echo $((1 + 1))           #=> 2
echo $((2 ** 3))          #=> 8
i=10 && echo $((i % 4))   #=> 2

[shell] Bashで乱数

Bashのシェル変数にはRANDOMという変数があり、これを参照すると0~32767のランダムな整数を得られる。

$ for n in {1..10}; do echo $RANDOM; done
12349
4092
31967
6007
10451
27446
25349
2318
12177
23783