5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

オブジェクトの状態を一時的に変化させる処理を簡潔に書くためにusingステートメントを活用してみる

Last updated at Posted at 2021-10-22

モチベーション

  • 「オブジェクトの状態を、ある一定期間変化させ、その後元に戻す」という処理を簡潔に書きたい。

例:WinFormsコントロールのEnabledプロパティを動的に切り替える

例えば、Form1button1ボタンが置かれていて、ボタンをクリックした際、ある特定の処理が終わるまでボタンを押下不能にしたいとします。ベタに書けば、例えば以下のようなコードが考えられます。

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private async void button1_Click(object sender, EventArgs e)
    {
        button1.SetEnabled(false);
        await Task.Delay(5000);
        button1.SetEnabled(true);
    }
}

ここでSetEnabledメソッドは次のような拡張メソッドです。

static class ControlExtensions
{
    public static void SetEnabled(this Control control, bool enabled)
    {
        if (control.InvokeRequired)
        {
            control.Invoke(new Action(() => control.Enabled = enabled));
        }
        else
        {
            control.Enabled = enabled;
        }
    }
}

とりあえずはこれで十分なのですが、いちいちEnabledプロパティをfalseにしたりtrueに戻したりといった記述が面倒くさい。こういった処理を少しでも簡潔に書けないかと思い、次のような方法を考えてみました。

まず、コンストラクタ内でEnabledfalseに設定し、Disposeメソッドでtrueに戻す処理を記述した、IDisposableを実装したクラスを用意します。

class ControlNotEnabled : IDisposable
{
    private Control _control;
    public ControlNotEnabled(Control control)
    {
        _control = control;
        _control.SetEnabled(false);
    }

    #region IDisposable

    private bool disposed = false;
    public void Dispose()
    {
        if (!disposed)
        {
            _control.SetEnabled(true);
            disposed = true;
        }
    }

    #endregion
}

次に、上記のControlNotEnabledクラスのインスタンスを生成する拡張メソッドを用意します。

public static ControlNotEnabled NotEnabled(this Control control) => new(control);

これで準備が整いました。Form1クラスのbutton1_Clickを次のように書き換えます。

private async void button1_Click(object sender, EventArgs e)
{
    using (button1.NotEnabled())
    {
        await Task.Delay(5000);
    }
}

このコードは、usingステートメントのコードブロックに入る直前に、ControlNotEnabledクラスのコンストラクタによってbutton1.SetEnabled(false)が呼び出され、ブロックを抜けた直後に同クラスのDisposeメソッドによってbutton1.SetEnabled(true)が呼び出されます。これによって、usingコードブロック内でのみbutton1.Enabledfalseになるという処理を記述することができました。

汎用化する

上記の方法は汎用化できそうです(あくまで使用者が使い方を知っていることが前提ですが)。汎用化のためには、まず以下のような抽象クラスを用意します。

public abstract class ObjectState<T> : IDisposable where T : class
{
    private T _value;

    protected ObjectState(T value)
    {
        _value = value;
        Set(_value);
    }

    protected abstract void Set(T value);

    protected abstract void Reset(T value);

    #region IDisposable

    private bool disposed = false;
    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                Reset(_value);
            }

            disposed = true;
        }
    }

    public void Dispose() => Dispose(true);

    #endregion
}

ネーミングが安直ですね。いい名前が思いつきませんでした。それはともかくとして、これを使えば、先程のControlNotEnabledクラスは以下のように書き換えることができます。

class ControlNotEnabled : ObjectState<Control>
{
    public ControlNotEnabled(Control control) : base(control) { }

    protected override void Set(Control value)
    {
        value.SetEnabled(false);
    }

    protected override void Reset(Control value)
    {
        value.SetEnabled(true);
    }
}

ObjectState<T>クラスは、ただのクラスなので、usingステートメントなど使わなくてもいろいろな場面で役に立ちそうな気がしました。とりあえず個人的には、本記事のようなusingステートメント前提にした使用を想定しています。

さらに汎用化する(2021/10/31追記)

コメントにて以下のご意見をいただきました。

ところでこういう場合Disposeに期待するのは「using前の状態に戻す」だと思うので、抽象クラスの方でusing前の値を覚えておくように改造してみました。
こうすることでControlNotEnabeledのusingが入れ子になったときも期待される動きになると思います。

この方法を考えた当時、using前の状態に戻すという発想というか、入れ子になった場合どうするかを一切考えていませんでした。このコメントと改造例を参考に、抽象クラスを以下のように変更してみました。

public abstract class ObjectState<TTarget, TState> : IDisposable where TTarget : class
{
    private TTarget _target;
    private TState _state;

    protected ObjectState(TTarget target, TState value)
    {
        _target = target;
        _state = GetState(_target);

        SetState(_target, value);
    }

    protected abstract TState GetState(TTarget target);

    protected abstract void SetState(TTarget target, TState value);

    #region IDisposable

    private bool disposed = false;
    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                SetState(_target, _state);

                disposed = true;
            }
        }
    }

    public void Dispose() => Dispose(true);

    #endregion
}

TState GetState(TTarget target)はオブジェクトの現在の状態を取得するメソッドです。これをコンストラクタ内で呼び出し、usingブロックに入る直前の状態を保持しておきます。
次に、void SetState(TTarget target, TState value)ですが、このメソッド1つでオブジェクトに対する状態セットを汎用化し、コンストラクタおよびDispose()において使用します。TTarget型のインスタンスは抽象クラスで持たない方がスッキリする、というご意見を頂きましたが、今の所個人的にこの方法での汎用化が一番使いやすいので、とりあえずこうしておきます。

上記の抽象クラスを用いて、ControlNotEnabledを書き直すと、以下のようになります。

class ControlNotEnabledState : ObjectState<Control, bool>
{
    public ControlNotEnabledState(Control target) : base(target, false) { }

    protected override bool GetState(Control target)
    {
        return target.Enabled;
    }

    protected override void SetState(Control target, bool value)
    {
        target.SetEnabled(value);
    }
}

機能的には最初の実装と同じです。

TStateには任意の型を指定できます。例としてはまたControl関連ですが以下のような使い方ができます。

class ControlTextAndColors
{
    public string Text { get; }
    public Color BackColor { get; }
    public Color ForeColor { get; }

    public ControlTextAndColors(string text, Color backColor, Color foreColor)
    {
        Text = text;
        BackColor = backColor;
        ForeColor = foreColor;
    }
}

class ControlTextAndColorsChangeState : ObjectState<Control, ControlTextAndColors>
{
    public ControlTextAndColorsChangeState(Control target, ControlTextAndColors value)
        : base(target, value) { }

    protected override ControlTextAndColors GetState(Control target)
    {
        return new ControlTextAndColors(target.Text, target.BackColor, target.ForeColor);
    }

    protected override void SetState(Control target, ControlTextAndColors value)
    {
        if (target.InvokeRequired)
        {
            target.Invoke(new Action(() =>
            {
                target.Text = value.Text;
                target.BackColor = value.BackColor;
                target.ForeColor = value.ForeColor;
            }));
        }
        else
        {
            target.Text = value.Text;
            target.BackColor = value.BackColor;
            target.ForeColor = value.ForeColor;
        }
    }
}

static class ControlExtensions
{
    public static ControlTextAndColorsChangeState TextAndColorsChangedState(this Control control, string text, Color backColor, Color foreColor)
    {
        return new ControlTextAndColorsChangeState(control, new ControlTextAndColors(text, backColor, foreColor));
    }

    public static ControlTextAndColorsChangeState TextChangedState(this Control control, string text)
    {
        return new ControlTextAndColorsChangeState(control, new ControlTextAndColors(text, control.BackColor, control.ForeColor));
    }

    public static ControlTextAndColorsChangeState BackColorChangedState(this Control control, Color backColor)
    {
        return new ControlTextAndColorsChangeState(control, new ControlTextAndColors(control.Text, backColor, control.ForeColor));
    }
}

これらを使って、以下のようにusingを入れ子にしてみます。

private async void button1_Click(object sender, EventArgs e)
{
    using (button1.NotEnabled())
    {
        await Task.Delay(1000);

        using (button1.TextChangedState("Text Changed"))
        {
            await Task.Delay(1000);

            using (button1.BackColorChangedState(Color.Orange))
            {
                await Task.Delay(1000);
            }

            await Task.Delay(1000);
        }

        await Task.Delay(1000);
    }
}

これを実行してみてください。まず押下不能になり、1秒後にbutton1Textが変わり、1秒後にBackColorが変わり、1秒後にBackColorが元に戻り、1秒後にTextが元に戻り、最後に1秒後に押下可能な状態に戻るはずです。

まとめ

usingステートメントは、Dispose()メソッドによる「リソースの破棄」というところに目が行きがちですが、考え方しだいでいろいろな使い方ができるな、と思いました。

5
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?