- 개선 전 코드
- 일반적인 비동기로 처리
- IDisposable 활용
- IAsyncDisposable 활용
- ProgressButtonAlone class
- ProgressButton class
- Reference
버튼을 클릭하고 이벤트 핸들러에서 내용을 처리 중일 때는 버튼을 잠시 잠그고 처리 완료 후 버튼의 잠금을 해제하여 이중 클릭 방지와 처리 과정이 동작 중임을 가시적으로 표현하곤 한다. 이 때에 필요한 방법을 4가지 정도로 소개할까 한다. 테스트 환경은 .NET 8.0 WPF 이다.
일반적으로 간단하게 처리하고자 한다면 아래와 같이 코드를 작성할 것이다.
개선 전 코드
public static void ButtonOnTest(object sender, RoutedEventArgs e)
{
if (sender is not Button btn)
{
return;
}
string? orgContent = btn.Content?.ToString();
try
{
btn.Content = "처리중...";
btn.IsEnabled = false;
// 해당 작업이 있다고 가정
Thread.Sleep(5000);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message); // Log처리 가정
}
finally
{
btn.Content = orgContent;
btn.IsEnabled = true;
}
}
위의 코드는 문제가 2가지 정도 있는데 하나는 동작 중에 화면이 잠기는 것이고 두 번째는 이중 클릭을 방지할 수 없다는 것이다. 아래의 코드는 비동기 방식으로 처리하여 간단하게 개선한 것이다.
일반적인 비동기로 처리
public static async Task ButtonOnTest(object sender, RoutedEventArgs e)
{
if (sender is not Button btn)
{
return;
}
string? orgContent = btn.Content?.ToString();
try
{
btn.Content = "처리중...";
btn.IsEnabled = false;
// 해당 작업이 있다고 가정
await Task.Delay(5000);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message); // Log처리 가정
}
finally
{
btn.Content = orgContent;
btn.IsEnabled = true;
}
}
문제는 해결된 듯 보이지만 여기에 진행 과정이라든지 finally 부분을 필수적으로 사용해야 한다든지 등의 불편한 점을 해결해 보고 굳이 아래의 4가지 정도의 방법은 사용하지 않더라도 학습에 도움이 될 만한 것들이다.
IDisposable 활용
public sealed class DisposeButton : IDisposable
{
private readonly Button mButton;
private readonly string mOrgContent;
public DisposeButton(Button? button, string? spinText = null)
{
ArgumentNullException.ThrowIfNull(button);
mButton = button;
mOrgContent = button.Content?.ToString() ?? string.Empty;
mButton.Content = string.IsNullOrWhiteSpace(spinText) ? "처리중..." : spinText;
mButton.IsEnabled = false;
}
~DisposeButton() => Dispose(false);
private void Dispose(bool disposing)
{
if (disposing)
{
mButton.Dispatcher.Invoke(() =>
{
mButton.Content = mOrgContent;
mButton.IsEnabled = true;
});
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
클릭 후 작업이 완료되면 자동으로 잠금 해제
public static async Task ButtonOnTest(object sender, RoutedEventArgs e)
{
using DisposeButton _ = new(sender as Button, "동작중...");
// 해당 작업이 있다고 가정
await Task.Delay(5000);
}
IAsyncDisposable 활용
public class DisposeButtonAsync : IAsyncDisposable
{
private readonly Button mButton;
private readonly Func<Task>? mFunc;
private readonly string mOrgContent;
public DisposeButtonAsync(Button? button, Func<Task>? fn = null)
{
ArgumentNullException.ThrowIfNull(button);
mButton = button;
mFunc = fn;
mOrgContent = button.Content?.ToString() ?? string.Empty;
mButton.Content = "처리중...";
mButton.IsEnabled = false;
}
#pragma warning disable CA1816
public async ValueTask DisposeAsync()
#pragma warning restore CA1816
{
try
{
await (mFunc?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
}
catch (Exception ex)
{
LogHelper.Logger.Error($"DisposeButtonAsync(DisposeAsync) : ERROR : {ex.Message}");
}
finally
{
try
{
await mButton.Dispatcher.InvokeAsync(() =>
{
mButton.Content = mOrgContent;
mButton.IsEnabled = true;
});
}
catch (Exception ex)
{
LogHelper.Logger.Error($"DisposeButtonAsync(Dispatcher.InvokeAsync) : ERROR : {ex.Message}");
}
}
}
}
Func 델리게이트 사용
public static async Task ButtonOnTest(object sender, RoutedEventArgs e)
{
await using DisposeButtonAsync _ = new(sender as Button, async () =>
{
// 해당 작업이 있다고 가정
await Task.Delay(5000);
});
}
참고로 IDisposable, IAsyncDisposable, GC.SuppressFinalize(this)의 관계를 아래에 요약(Gemini 2.0)했다. Microsoft에서 설명한 방법과 비교해 보길 바란다.1
인터페이스 | 파이널라이저 | GC.SuppressFinalize(this) |
---|---|---|
IDisposable | 필요할 수 있음 | Dispose()에서 호출 필요 |
IAsyncDisposable | 거의 필요 없음 | 사용하면 안 됨 |
아래의 두 가지 방법은 Singleton으로 클래스를 만들어서 사용하고 내용 중에 프로그래스바의 형태로 표현하기 위해 [" ⦁ ", " ⦁⦁ ", " ⦁⦁⦁ ", " ⦁⦁⦁⦁ ", " ⦁⦁⦁⦁⦁"]
형태를 버튼 Content에 표현한다. 이 부분을 실제 프로그래스바로 바꾸어도 무방하다.
ProgressButtonAlone class
이 클래스를 적용한 모든 버튼은 하나의 버튼이 동작 중일 때는 다른 모든 버튼은 ‘사용중…’ 이라는 내용이 버튼의 Content 영역에 나타난다. 그리고 동작이 완료된 다음에 다른 버튼도 동일한 동작 방식으로 처리한다.
public static async Task ButtonOnTest(object sender, RoutedEventArgs e)
{
await ProgressButtonAlone.Go.ExecuteAsync(sender as Button, async () =>
{
// 해당 작업이 있다고 가정
await Task.Delay(5000);
});
}
ProgressButton class
이 클래스는 위와 다르게 모든 버튼이 각각 독립적으로 동작한다.
public static async Task ButtonOnTest(object sender, RoutedEventArgs e)
{
await ProgressButton.Go.ExecuteAsync(sender as Button, async () =>
{
// 해당 작업이 있다고 가정
await Task.Delay(5000);
});
}
ProgressButtonAlone, ProgressButton 소스는 Github에 올려두었으니 해당 링크를 참고하기 바란다.