파일을 복사하는 프로그램을 만들어보자.
이렇게 UI를 구성하고 버튼을 클릭하면 빌드한 파일에 있는 경로의 파일을 복사하게 할 것이다. 그러기 위해서 파일을 복사하는 class를 만들자.
namespace demo
{
internal class FIleManager
{
private FIleCopyForm form;
public FIleManager(FIleCopyForm form)
{
this.form = form;
}
public void Copy(string srcfile, string destfile)
{
byte[] buffer = new byte[1024];
int pos = 0;
var fi = new FileInfo(srcfile);
var filesize = fi.Length;
using (BinaryReader rd = new BinaryReader(File.Open(srcfile, FileMode.Open)))
using (BinaryWriter wr = new BinaryWriter(File.Open(destfile, FileMode.Create)))
{
while (pos < filesize)
{
int count = rd.Read(buffer, 0, buffer.Length);
wr.Write(buffer, 0, count);
pos += count;
double pct = (pos / (double)filesize) * 100;
Console.WriteLine(pct);
form.progressBar1.Value = (int)pct;
form.lblPct.Text = String.Format("{0} %", (int)pct);
}
}
}
}
}
파일을 복사하는 FileManager class이다. 간단하게 buffer를 만들어서 buffer에 파일을 읽고 가득 차면 복사하고 다시 buffer를 비우고 다시 파일을 읽는 것을 계속 반복한다.
경로에 복사할 src 파일을 만들고 (dest 파일은 이미 실행해서 생김)
private void btnCopy_Click(object sender, EventArgs e)
{
FIleManager fm = new FIleManager(this);
fm.Copy("src.mp4", "dest.mp4");
}
버튼 이벤트 리스너에 등록을 하면 구현이 완료된다.
하지만 여기서 파일을 복사하는 중에 윈도우 프로그램을 움직이거나 다른 이벤트를 발생시키면 UI의 변화가 보이지 않거나 프로그램이 멈춘 것처럼 보이는 UI Freeze가 발생한다.
이는 UI thread에서는 UI를 그리는 일을 해야 하는데 파일을 복사하는 로직을 수행하느라 순간적으로 UI를 그리는 일을 멈추기 때문에 발생하는 현상이다. 이를 해결하기 위해서는 비즈니스 로직(복사)을 처리하는 새로운 thread를 만들어서 worker thread가 로직을 수행하게 하고 UI thread는 UI를 그리는데에만 집중하게 하면 된다.
private void btnCopy_Click(object sender, EventArgs e)
{
Thread t = new Thread(FileCopy);
t.Start();
}
private void FileCopy()
{
FIleManager fm = new FIleManager(this);
fm.Copy("src.mp4", "dest.mp4");
}
이렇게 파일을 복사하는 부분은 FileCopy()라는 메소드로 빼고 Thread를 생성해서 Thread에 실행할 메소드를 넘겨주고 실행을 하게 되면은 worker thread가 파일 복사 로직을 담당하기 때문에 UI를 변경해도 UI Freeze가 발생하지 않는다.
하지만 아직도 문제가 있다. FIle을 복사해주는 FIleManager가 특정 UI에 의존하고 있다는 점이다.
namespace demo
{
internal class FIleManager
{
private FIleCopyForm form;
public FIleManager(FIleCopyForm form)
{
this.form = form;
}
이렇게 특정 form에 의존하고 있다. 이를 해결해주자.
이벤트는 클래스내에 특정한 일(event)이 있어났음을 외부의 이벤트 가입자(subscriber)들에게 알려주는 기능을 한다. C#에서 이벤트는 event라는 키워드를 사용하여 표시하며, 클래스 내에서 일종의 필드처럼 정의된다.
이벤트에 가입하는 외부 가입자 측에서는 이벤트가 발생했을 때 어떤 명령들을 실행할 지를 지정해 주는데, 이를 이벤트 핸들러라 한다. 이벤트에 가입하기 위해서는 += 연산자를 사용하여 이벤트핸들러를 이벤트에 추가한다. 반대로 이벤트핸들러를 삭제하기 위해서는 -= 연산자를 사용한다. 하나의 이벤트에는 여러 개의 이벤트핸들러들을 추가할 수 있으며, 이벤트가 발생되면 추가된 이벤트핸들러들을 모두 차례로 호출한다.
// 클래스 내의 이벤트 정의
class MyButton
{
public string Text;
// 이벤트 정의
public event EventHandler Click;
public void MouseButtonDown()
{
if (this.Click != null)
{
// 이벤트핸들러들을 호출
Click(this, EventArgs.Empty);
}
}
}
// 이벤트 사용
public void Run()
{
MyButton btn = new MyButton();
// Click 이벤트에 대한 이벤트핸들러로
// btn_Click 이라는 메서드를 지정함
btn.Click += new EventHandler(btn_Click);
btn.Text = "Run";
//....
}
void btn_Click(object sender, EventArgs e)
{
MessageBox.Show("Button 클릭");
}
redux에서 store를 구독하는 시스템이랑 비슷하다. 특정 class 내에서 변화가 발생한 것을 다른 class 및 form에게 알리는 방식이다.
그런데 이걸 알기 전에 2가지의 개념을 알아야한다. deletgate와 EventHandler이다.
우리가 정수를 담을 변수로는 int형 변수를 선언한다. int type에는 부동소수나 문자열이 담길 수 없다. 왜냐면 변수를 int로 선언했기 때문이다. 자 그러면 함수를 변수로 담을 때는 어떻게 해야할까
int Func = fun1
public void fun1() {
Console.Write("hello")
}
콘솔에 hello를 찍는 함수를 정수형 변수에 담을 수 있을까? 아니다. 그러면 함수는 어디에다가 담아야할까? 여기서 delegate가 사용된다.
namespace ConsoleApp1
{
public class Example
{
public delegate void MyDelegate();
public static void HelloyByKorea()
{
Console.WriteLine("안녕!");
}
public static void HelloByEnglish()
{
Console.WriteLine("hello!");
}
public static void Hello(MyDelegate myDeletagate)
{
myDeletagate();
}
public static void Main(string[] args)
{
int age = 30;
if(age < 20)
{
Hello(HelloyByKorea);
}
else
{
Hello(HelloByEnglish);
}
}
}
}
예제 코드이다.
먼저 delegate type으로 함수를 작성하듯이 변수를 선언한다. 리턴타입, 인자에 대한 prototype을 정의하면 해당 delegate와 형식이 같은 함수는 delegate에 담길 수 있다.
위 예제는 20살 이하면은 한글로, 20살 이상이면 영어로 인사를 하는 함수이다. HelloyByKorea, HelloByEnglish 메소드를 작성하고 해당 메소드를 콜백함수로 받아서 실행해주는 Hello 메소드를 만들었다. Hello method는 인자로 delegate를 받고 있다.
메인에서는 나이에 따라(비즈니스 로직) Hello()의 인자로 HelloyByKorea, HelloByEnglish를 넘겨준다. 그러면 깔끔하게 비즈니스 로직에 따라 다른 결과를 보여줄 수 있다.
이벤트 핸들러는 발생된 이벤트에 대해서 리턴값이 없는 이벤트를 나타낸다.
이벤트 핸들러의 기본적인 형태는 아래와 같다.
public delegate void EventHandler(object? sender, EventArgs arg);
이벤트 핸들러의 동작원리는 다음과 같다.
이로써 처음에 다루고자 한 Form에 대한 의존성을 제거함과 동시에 모든 Form에 대하여 확장이 가능한 이벤트가 되었다. 이것을 직접 적용해보면,
namespace demo
{
internal class FIleManager
{
// 이벤트를 다루는 이벤트 핸들러 선언 (이벤트의 종류는 double)
// 다루고자 하는 이벤트 (double type)에 변화가 생기면 현재 class와 변화된 이벤트 값을
// 구독하는 함수에게 전달
// delegate처럼 이벤트를 (Object sender, dobule e)로 받아들이는 함수들만 이벤트 핸들러를 구독할 수 있음
public event EventHandler<double> InProgress;
public void Copy(string srcfile, string destfile)
{
...
{
while (pos < filesize)
{
int count = rd.Read(buffer, 0, buffer.Length);
wr.Write(buffer, 0, count);
pos += count;
double pct = (pos / (double)filesize) * 100;
if (InProgress != null)
{
InProgress(this, pct); // 이벤트를 발생. 기존에 핸들러에 등록된 이벤트(double type)값에 변화를 줌 => 모든 등록된 구독자들에게 이벤트가 변경되었음을 (Object sender ,double event)형태로 전달
}
}
}
}
}
}
이렇게 된다. 선언한 핸들러 객체를 함수처럼 사용해서 1번째 인자로 현재 호출자를, 2번째 인자로 변경하고자 하는 이벤트 값을 제공을 하면
private void btnCopy_Click(object sender, EventArgs e)
{
Thread t = new Thread(FileCopy);
t.Start();
}
private void FileCopy()
{
FIleManager fm = new FIleManager();
fm.InProgress += Fm_InProgress; // 이벤트 핸들러를 구독
fm.Copy("src.mp4", "dest.mp4");
}
// 이벤트에 변화가 있을 시에 실행할 함수, 구독 시에 인자로 전달함
private void Fm_InProgress(object sender, double e)
{
if(InvokeRequired)
{
Invoke(new EventHandler<double>(Fm_InProgress), sender, e);
}
else
{
this.progressBar1.Value = (int)e;
this.lblPct.Text = String.Format("{0} %", (int)e);
}
}
이벤트를 구독하고 있는 Fm_InProgress 함수가 이벤트 핸들러에 의해 호출되면서 원하는 비즈니스 로직이 된다. InvokeRequired는 현재 스레드가 UI 스레드인지 아닌지를 구분하는 boolean 값이다. 현재 스레드가 UI 스레드 일 경우에는 InvokeRequired가 true가 되면서 새로운 worker thread를 만든다.
이렇게 하여 worker thread를 만들어서 파일 복사 비즈니스 로직을 수행하게 하여 UI freeze를 방지하고, 다루고자 하는 데이터(변경 가능성이 있고 외부에게 알려야하는 데이터)를 EventHandler로 다루면서 FileManager에 있었던 Form에 대한 의존성을 제거했다.