Console.ReadLine(bool intercept ) が欲しい!

と、思いませんか? うまいもんはうまい。by はや

コンソールなプログラムを C# で組んでいると、パスワードなど見せてはいけないものを入力するケースって意外にありますよね。そういうとき、Linuxとかだと、シェルスクリプトで stty でファイトー、一発!!ってなもんですが、かなしきかな、C#(というより、.Net Frameworkのクラスライブラリ)には、適当なものがありませぬ。

クラスライブラリの広大な海から、やっとこさ、System.Console.ReadKey(bool)というものを見つけました。が、やっぱりその辺は自作しないといけないようで・・・。Console.ReadKey(bool)ってのがあるんだから、Console.ReadLine(bool)くらい作っておいて欲しい・・・そこまで頼るな!って?

どうでもいいけど、拡張メソッドの静的バージョンって欲しいよう(^^;;; Consoleクラスに自作静的メソッドをバカバカ追加して自己満に浸りたい・・・。

愚痴はさておき・・・忘れないようにコード・メモです

  /// <summary>
  /// コンソール関連のヘルパー&ラッパー関数など。C#4.0必須
  /// </summary>
  public static class ConsoleEx
    {
      /// <summary>
      /// メッセージを出力してキー入力を待機します。
      /// </summary>
      public static ConsoleKeyInfo WriteLineAndReadKey(string msg = "\n何かキーを押してください。",bool echoback = false)
        {
           Console.WriteLine(msg);
           return Console.ReadKey(echoback);
        }
      public static string WriteAndReadLine(string mesg,bool intercept = false)
        {
          if(!string.IsNullOrEmpty(mesg))
              Console.Write(mesg);

          return ReadLine(intercept);
        }

      /// <summary>
      ///  エコーバック無しにキー入力を行う。(たぶん日本語はだめ)
      /// </summary>
      /// <returns>入力値</returns>
      public static string ReadLine(bool intercept,char mask = '*')
        {
          if(!intercept)
            return Console.ReadLine();

          var cs = new List<char>();
          bool isContinue = true;
          while(isContinue)
            {
              var ki = Console.ReadKey(true);
              switch(ki.Key)
                {
                  case ConsoleKey.Escape:
                    cs.Clear();
                    goto case ConsoleKey.Enter;
                  case ConsoleKey.Enter:
                    isContinue = false;
                    break;
                  case ConsoleKey.Backspace:
                    if(cs.Count > 0)
                      {
                        cs.RemoveAt(cs.Count - 1);
                        Console.Write("\b \b");
                      }
                    break;
                  default:
                    if(ki.KeyChar != '\0')
                      cs.Add(ki.KeyChar);
                    Console.Write(mask);
                    break;
                }
            }
          return new string(cs.ToArray());
        }
    }

追記:バグ修正 (2017/05/17)

フォーム間の依存関係を最小限に

C#備忘ログ

C#でWindowsフォームを使うようなアプリケーションは殆ど書かないのであまり興味もなかったのですが、今回2つのフォームを使うツールを書いたときに困ったことがあった。

フォームA(class FormA : Form) / フォームB(class FormB : Form) の2つがあって、

1)フォームA内に配置したボタン(Button1)をクリックすると、フォームBを生成すると同時にフォームAは非表示へ。
2)フォームBを閉じると、フォームAを表示する。

通常ならたぶん、フォームAの button1の Clickイベントハンドラに フォームBをnewして Show()メソッドをコール。

//子フォームを生成して表示、自身は非表示にする
private void Button1_Click(object sender, EventArgs e)
{
  this.Hide();
  var formB = new FormB();
  formB.Show(this);
}

そしてフォームBを閉じたり、非表示になった時のイベントハンドラ(FormB_FormClosingとかFormB_VisibleChangeとか)でFormAのShow()メソッドをコール。

//親フォームを表示(復帰)
private void Form1_FormClosing(Object sender, FormClosingEventArgs e)
{
  this.Owner.Show();
}

のような感じになると思うんですが、フォームBを閉じたときに、フォームAを表示するだけじゃなくて、フォームA上のbutton1のTextプロパティの表示も変えたい!とか、button1のEnabledプロパティーを・・・とか、にしたいとき、FormA側にpublicなメソッドを一個追加してコールさせなければならない・・・。

つまり、フォームAの勝手な都合で、全然関係のないフォームBのソースも修正しなければならなくなる。フォームA,フォームBそれぞれがお互いのインスタンスを共有しなければならない、という非常に非効率な状態になる。2つのフォームだけならまだなんとかなるけど、フォーム10個とかになったときの事を考えると恐ろしい。。。

この場合の解決方法はやはり・・・フォームA側から直接フォームBのイベントに+= 演算子でラムダ式を注入するのが一番いい。Showメソッドで自分自身のインスタンスを渡す必要もない。フォームBのソースを汚染することもなくなる。

ま、ちゃんとメソッドを定義して・・・いう書き方の方がいいのか、インラインでラムダ式を放り込む方がいいのか、書き手側の好みの問題でしょうか。

//FormA
private void Button1_Click(object sender, EventArgs e)
{
  this.Hide();
  var formB = new FormB();
  formB.Show();

  //ラムダ式を注入
  formB.FormClosing += ( bs, be ) => {  Show(); button1.Text = "フォームBは閉じられた!";  };
  //デリゲートを追加
  formB.FormClosing += formBClosed; 
}
private void formBClosed(object sender, FormClosedEventArgs e)
{
    Show();
    button1.Text = "フォームBは閉じられた!";
}

また、FormBの子コントロールの特定のイベントにイベントハンドラを注入したければ、子コントロールにもフォームA側からイベントハンドラを注入してあげればいいが、フォームの子コントロールのアクセスレベルは private なので、単純に子コントロールへはアクセスできない。
そういうときは、フォームBのControlsプロパティから目的の子コントロールを得られる。

//FormA 
private void Button1_Click(object sender, EventArgs e)
{
  this.Hide();
  var formB = new FormB();
  formB.FormClosing += ( bs, be ) => {  Show();  };
  formB.Show();
  var ctrl = formB.Controls["button2"]);
  ctrl.Click += (bs,be) => button1.Text = "クリックされたよ";
}
//エラー処理はしていないよ

メインフォーム ⇔ 子フォーム間の依存関係はできるだけ避けたいし、子フォーム間同士の依存関係もできるだけ避けたい。お互いのフォームがお互いのインスタンス(正確にはインスタンスへの参照)を持ってコードを書くとフォームが増えるに従って グチャグチャになってしまう気がする。。。
気をつけよう。

ファイル名に使えない文字

C# 備忘ログ。

テキストボックスに入力したワードをそのまま ディレクトリの作成やファイル作成とかのメソッドに渡すと、例外が飛んでくることがある。ほとんどの場合、?,:,/ などの文字をエスケープしてない!のが原因。めんどくさいのう! と思いながら、それらの文字をフィルタリングするメソッドを書いてたんですが・・・結構気の利くメソッドを見つけた。

Path.GetInvalidFileNameChars()とPath.GetInvalidPathChars()メソッド。

単純にコールすると、ファイルシステム上で使えない文字をchar[]配列に格納して返してくれる! ・・・が、ドキュメントを見ると、頼りないお言葉が・・・

このメソッドから返される配列は、ファイルおよびディレクトリの名前に無効な文字の完全なセットが含まれているとは限りません。

おいおい!仕事放棄するなよぅっっっッ!
ってなわけで、結局、”CON”やら”PRN”とかは自前でフィルタリングしないといけない・・・と。

//using System;
//using System.Text.RegularExpressions;
//using System.IO;
//using System.Linq;
//...
/// <summary>
/// ファイルシステムで使用できない文字をエスケープする。
/// CON,PRNなどの文字列の場合は、before,afterを前後に付けて返す。
/// 制御文字などの文字は、16進文字コードに変換する。
/// </summary>
/// <param name="str">検査する文字</param>
/// <returns>エスケープされた文字列</returns>
public static string EscapeInvalidPathChars(string str,string before="x",string after = "x")
  {
    string rv = Regex.Replace(str,
                              string.Format(@"([{0}])",Regex.Escape(new String(Path.GetInvalidFileNameChars()))),
                              m => Uri.HexEscape(m.Value.First()));

    if(Regex.Match(rv,@"^(CON|PRN|AUX|NULL|COM\d|LPT\d)$",RegexOptions.IgnoreCase).Success)
      rv = string.Format("{1}{0}{2}",rv,before,after);

    return rv;
  }

そういえば・・・CONCON問題とかあったのう。。。ブルースクリーンで一喜一憂してた時代でほのぼのしてたなー