モチベーション
- 「オブジェクトの状態を、ある一定期間変化させ、その後元に戻す」という処理を簡潔に書きたい。
例:WinFormsコントロールのEnabled
プロパティを動的に切り替える
例えば、Form1
にbutton1
ボタンが置かれていて、ボタンをクリックした際、ある特定の処理が終わるまでボタンを押下不能にしたいとします。ベタに書けば、例えば以下のようなコードが考えられます。
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
に戻したりといった記述が面倒くさい。こういった処理を少しでも簡潔に書けないかと思い、次のような方法を考えてみました。
まず、コンストラクタ内でEnabled
をfalse
に設定し、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.Enabled
がfalse
になるという処理を記述することができました。
汎用化する
上記の方法は汎用化できそうです(あくまで使用者が使い方を知っていることが前提ですが)。汎用化のためには、まず以下のような抽象クラスを用意します。
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秒後にbutton1
のText
が変わり、1秒後にBackColor
が変わり、1秒後にBackColor
が元に戻り、1秒後にText
が元に戻り、最後に1秒後に押下可能な状態に戻るはずです。
まとめ
using
ステートメントは、Dispose()
メソッドによる「リソースの破棄」というところに目が行きがちですが、考え方しだいでいろいろな使い方ができるな、と思いました。