Windowsコントロールのイベントを一時的に無効化したい (Windowsフォーム編)

本日のテーマ

  • 起動時に、画面の各コントロールに初期値が設定されるアプリについて考える
  • 初期値を設定している間だけ、コントロールのイベントを無効にしたい

要するに、TextChangedを一時的に黙らせたい。

背景

かつて、僕が某システムを開発していたとき、「アプリケーション画面の起動が遅い」という問題に直面したことがありました。

原因の一つは、画面のコントロールに初期値*1を設定する際、不要なイベントが一斉に走るせいでパフォーマンスが悪化していたためでした。

すぐさま、不要なイベントを一時的に無効化する処置がとられました。

さて、イベントの一時無効化は開発現場のあるあるパターンですが、力技の汚い実装でコードをとっ散らかす開発メンバが多いと感じたので、とりあえずここに僕がよく使う方法を書き残しておくことにします。

開発環境

この記事を書くために使用した開発環境は、Windows 8.1*2 + Visual Studio 2013 です。

言語は C# 5.0 です。

この記事が対象とするアプリケーションの種類は、「Windowsフォームアプリケーション」です。WPFには対応していないのでご注意ください。

WindowsFormExtendedクラス

指定したコントロールのイベントを一時的に無効化するためのWindowsFormExtendedクラスを作りました。もうちょっとマシなネーミングにすればよかった。

Publicなメソッドは、DoSomethingWithoutEvents(Control control, Action action)たったひとつです。

  • 第一引数には、イベントを一時的に無効化したいコントロールを指定します
  • 第二引数には、イベントを無効化している間に実行したい処理を指定します

つかいかた

  1. WindowsフォームアプリケーションにWindowsFormExtendedを組み込みます。
  2. 以下のように書くと、textBox1に紐づくあらゆるイベントが一時的に無効化された状態で、ラムダ式が実行されます。
WindowsFormExtended.DoSomethingWithoutEvents(
    textBox1,                           // イベントを無効化したいコントロール
    () => textBox1.Text = "初期値");    // 実行したい処理

なお、第一引数に指定したコントロールが子コントロールを持つ場合は、子コントロールのイベントも無効化の対象となります。

たとえば第一引数にフォーム自身を指定すると、フォーム内のすべてのコントロールのイベントが一時的に無効になります。

ソースコードはこんな感じです。

WindowsFormExtended.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Windows.Forms;

namespace Sample0711
{
    public static class WindowsFormExtended
    {
        /// <summary>
        /// 指定したコントロールのイベントを一時的に無効化した状態で、
        /// 所定の処理を実行します
        /// </summary>
        /// <param name="control">対象コントロール</param>
        /// <param name="action">実行したいイベント</param>
        public static void DoSomethingWithoutEvents(Control control, Action action)
        {
            if (control == null) throw new ArgumentNullException();
            if (action == null) throw new ArgumentNullException();

            var eventHandlerInfo = RemoveAllEvents(control);
            try     { action(); }
            finally { RestoreEvents(eventHandlerInfo); }
        }

        private static List<EventHandlerInfo> RemoveAllEvents(Control root)
        {
            var ret = new List<EventHandlerInfo>();
            GetAllControls(root).ForEach((x) => ret.AddRange(RemoveEvents(x)));
            return ret;
        }

        private static List<Control> GetAllControls(Control root)
        {
            var ret = new List<Control>() { root };
            ret.AddRange(GetInnerControls(root));
            return ret;
        }

        private static List<Control> GetInnerControls(Control root)
        {
            var ret = new List<Control>();
            foreach (Control control in root.Controls)
            {
                ret.Add(control);
                ret.AddRange(GetInnerControls(control));
            }
            return ret;
        }

        private static EventHandlerList GetEventHandlerList(Control control)
        {
            const string EVENTS = "EVENTS";
            const BindingFlags FLAG = BindingFlags.NonPublic | 
                BindingFlags.Instance | 
                BindingFlags.IgnoreCase;

            return (EventHandlerList)control.GetType().GetProperty(EVENTS, FLAG)
                .GetValue(control, null);
        }

        private static List<object> GetEvents(Control control)
        {
            return GetEvents(control, control.GetType());
        }

        private static List<object> GetEvents(Control control, Type type)
        {
            const string EVENT = "EVENT";
            const BindingFlags FLAG = BindingFlags.Static | 
                BindingFlags.NonPublic | 
                BindingFlags.DeclaredOnly;

            var ret = type.GetFields(FLAG)
                .Where((x) => x.Name.ToUpper().StartsWith(EVENT))
                .Select((x) => x.GetValue(control)).ToList();

            if (!type.Equals(typeof(Control))) ret.AddRange(GetEvents(control, type.BaseType));

            return ret;
        }

        private static List<EventHandlerInfo> RemoveEvents(Control control)
        {
            var ret = new List<EventHandlerInfo>();
            var list = GetEventHandlerList(control);

            foreach (var x in GetEvents(control))
            {
                ret.Add(new EventHandlerInfo(x, list, list[x]));
                list.RemoveHandler(x, list[x]);
            }
            return ret;
        }

        private static void RestoreEvents(List<EventHandlerInfo> eventInfoList)
        {
            if (eventInfoList == null) return;
            eventInfoList.ForEach((x) => x.EventHandlerList.AddHandler(x.Key, x.EventHandler));
        } 

        private sealed class EventHandlerInfo
        {
            public EventHandlerInfo(object key, EventHandlerList eventHandlerList, Delegate eventHandler)
            {
                this.Key = key;
                this.EventHandlerList = eventHandlerList;
                this.EventHandler = eventHandler;
            }
            public object Key                        { get; private set; }
            public EventHandlerList EventHandlerList { get; private set; }
            public Delegate EventHandler             { get; private set; }
        }
    }
}

まとめ

  • GUIのイベント制御はたいへん

いじょ。

*1:前画面から引き継いだ情報など。

*2:Surface Pro 3

Copyright (c) 2012 @tercel_s, @iTercel, @pi_cro_s.