BackgroundWorkerで、markdownのプレビュー処理を改善

前回までの作業で、画面で入力したMarkdown形式テキストをその場でプレビュー可能となりました。
ただ、この方法だとテキストのサイズが小さいときは問題ないのですが、サイズが大きくなってくるとプレビューに時間が掛かるため、キー入力に対するレスポンス低下が起きてしまいます。


今回は、テキストが大量になっても入力のレスポンスが低下しないように改造してみます。


リアルタイムプレビュー時の負荷を下げる


レスポンス低下の問題ですが、原因は2つ存在します。


  • 1文字でも変更があっると都度htmlへの変換を行う為、変換頻度が高すぎる。
  • htmlへの変換処理をイベントハンドラ内で行っている為、変換中にキー入力が出来ない

ここでは、それぞれの問題に対して対応を考えます。


変換頻度が高すぎる件の改善


まず、前者の変換頻度が高すぎる件ですが、TextChangedのイベントハンドラ内で直接Markdown形式からhtmlへの変換を行うのを止めます。
以下の処理に変更することで、変換の頻度を落とします。


  • テキストが変更されたタイミングでは、変更があった事を記録する(flgを立てる)だけにする
  • 一定周期のタイマーで、flgが立っているかチェックする。
  • flgが立っていれば変換を行う

それでは、実際に上記の改造を行ってみます。


前回までは、textBox1_TextChanged()で変換処理を直接行っていました。


private void textBox1_TextChanged( object sender, EventArgs e ) {
    // 変換ボタンが押されたことにする
    button1_Click( sender, e );
}


これを、flgを立てるよう変更します。


private bool changeFlg = false;
 
private void textBox1_TextChanged( object sender, EventArgs e ) {
    // 変換処理が必要なことを覚える
    changeFlg = true;
}



次に、タイマーコントロールをフォームに追加し、一定周期でイベントが走るようにします。(とりあえず1秒周期にしてみます)



コントロールを置いたら、イベントハンドラを追加します

private void timer1_Tick( object sender, EventArgs e ) {
    // 変換が不要な場合は何もしない。
    if ( !changeFlg ) {
        return;
    }
 
    // 変換処理を行う(変換ボタンを押したことにする)
    button1_Click( sender, e );
 
    changeFlg = false;
}




上記の改造後、再度プログラムを実行してみます。
改造の前に比べるとかなりレスポンスは向上しました(静止画では分かりませんが…)。


ただし、この状態でも、タイマーが走る1秒周期のタイミングで入力が引っかかる感じは残っています(テキストのサイズが大きい状態で編集を行うと顕著です)。


変換処理をイベント内で行っている件の改善


次は、Markdown形式の変換中にキー入力が出来ない為、引っかかりが残る件の対処です。


これは、タイマー処理のイベントハンドラがUIの入力イベントループの中で処理されていることが原因です(試しにタイマー処理内で、1秒程度スリープしてみると分かりやすいです)。


この問題に対してはC#(というか.Net Framework)の場合、BackgroundWorkerクラスを使用すると簡単に非同期処理を行うことが出来ます。


まずは、BackgroundWorkerコントロールを追加し、DoWork, RunWorkerCompletedイベントを追加します。
DoWorkイベントは、非同期で行うべき処理を記述します。イベント内では、GUIのコントロールに対して値をセットすることが出来ない点に注意が必要です。これは、スレッド間の競合が発生する恐れがある為です。実際に値をセットしようとすると、”実行時”に例外が発生します。

また、RunWorkerCompletedイベントの方は、DoWorkイベントが終了した際にコールされます。こちらは、GUI側のイベントループ内で走る処理ですので、画面コントロールに情報をセットすることが可能です。


ですので、BackgroundWorkerコントロールの基本的な流れは、DoWorkで時間の掛かる処理を行い、その後RunWorkerCompletedで処理結果を画面に反映するという流れになります。


で、具体的なプログラムの改造ですが、まずは、タイマーのイベントハンドラ処理を変更します。


private void timer1_Tick( object sender, EventArgs e ) {
    // 変換が不要な場合は何もしない。
    if ( !changeFlg ) {
        return;
    }
 
    // 変換処理を行う(変換ボタンを押したことにする)
    button1_Click( sender, e );
}




private void timer1_Tick( object sender, EventArgs e ) {
    // 変換が不要な場合は何もしない。
    if ( !changeFlg ) {
        return;
    }
 
    // すでに非同期処理中の場合は何もしない
    if ( backgroundWorker1.IsBusy ) {
        return;
    }
 
    // 変換処理を非同期で実施する
    backgroundWorker1.RunWorkerAsync( textBox1.Text );
}



backgroundWorker1のRunWorkerAsync()で、処理を非同期実行することが出来ます。
このメソッドを呼ぶと、別スレッドでbackgroundWorker1_DoWork()メソッドが実行されます。

また、RunWorkerAsyncメソッドに引数を渡すことで、DoWork()に対するパラメータを渡すことが可能です。引数は1つだけですがobject型なので、Dictionary<>やList<>をセットすれば、結果的に複数の情報を渡すことが可能です。今回必要な情報は1つだけなので、単純にstringの文字列を渡しています。


次に、実際に非同期処理を行うロジックをDoWork()に記述します。


private void backgroundWorker1_DoWork( object sender, DoWorkEventArgs e ) {
    //-------------------------
    // 入力パラメータを取得する
    //-------------------------
    string srcStr = e.Argument as string;
    if ( srcStr == null ) {
        srcStr = "";
    }
 
    //-------------------------
    // 変換処理を行う
    //-------------------------
    Markdown m = new Markdown();
    String destStr = m.Transform( srcStr ); 
 
    //-------------------------
    // 変換結果を返す
    //-------------------------
    e.Result = destStr;
}


先ほどRunWorkerAsync()で渡した情報は、DoWork()イベントの第二引数経由(e.Argument)で取得できます。e.Argumentは、当然ながらobject型なので、呼び元が渡したデータ型にキャストする必要があります。

キャストは、string srcStr =(string)e.Argument; といった形でも行うことが可能ですが、呼び元側で異なる型のオブジェクトを渡した場合、ここで例外が発生してしまいます。この為、”as”を使用したキャストを行います。asを使用した場合、元となるオブジェクトがキャストできない場合はnullが返る仕様なので、nullチェックを行ったうえで適切な処理を行います。


入力パラメータを取得したら、本来やりたかったMarkdownの変換を行った後、結果を返す必要があります。ここで、textBox2.Text = destStr;と書きたくなるところですが、前述したようにスレッド間の競合が発生する可能性があるので、この処理は認められません。

替わりに結果を、第二引数のResultプロパティ経由で渡します。こちらもArgumentと同様object型ですので、同様の配慮を行う必要があります。


最後は、変換結果を画面に反映させる処理です。
こちらは、BackgroundWorkerのRunWorkerCompleted()メソッドで行います。


private void backgroundWorker1_RunWorkerCompleted( object sender, RunWorkerCompletedEventArgs e ) {
    //--------------------------------------
    // textBoxに出力するために改行文字を変換
    //--------------------------------------
    String destStr = e.Result as String;
    if ( destStr == null ) {
        destStr = "";
    }
 
    String htmlText = GetHeader() + destStr.Replace( "\n", Environment.NewLine ) + GetFooter();
 
    //--------------------------------------
    // 変換結果をテキストで画面に表示
    //--------------------------------------
    textBox2.Text = htmlText;
 
    //--------------------------------------
    // 変換結果をブラウザにプレビュー
    //--------------------------------------
    webBrowser1.DocumentText =  htmlText;
}


DoWork()の時と同様、非同期処理の結果がe.Resultで取得できます。RunWorkerCompleted()はGUIのイベントループと同じスレッドで実行されるため、GUIコントロールにアクセスすることが可能です。

一方で、本メソッドで時間の掛かる処理を行うことは出来ません。ここで時間が掛かると、画面の応答速度が低下するため、操作性が悪くなってしまいます。


上記の改造を行ったうえで、再度プログラムを実行させると、無事、今まであった引っかかりがなくなりました。


以上で、前回作成したMarkdown変換処理のリアルタイムプレビューに対する処理改善は完了です。

関連記事

コメントを残す

メールアドレスが公開されることはありません。